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
//! Integration test for the reconcile source_id backfill (#1780).
//!
//! Why: `session_manager/tests.rs` was at the 1500-SLOC test cap; the
//! reconcile-backfill coverage lives here so neither file grows past its limit.
//! Keeping it in a focused sibling also makes the #1780 fix easy to locate.
//! What: seeds a record with `source_id=None` whose workspace is a real git repo
//! with a GitHub origin remote, runs `reconcile_on_boot`, then asserts the field
//! was populated. Uses `FakeTmuxDriver` from the sibling `tests` module.
//! Test: this file IS the test module; run with `cargo test -p trusty-mpm`.
use std::process::Command;
use chrono::Utc;
use tempfile::TempDir;
use super::manager::SessionManager;
use super::record::{ManagedSessionId, ManagedSessionState, SessionRecord};
use super::tests::FakeTmuxDriver;
/// Reconcile must backfill `source_id` from a workspace git remote (#1780).
///
/// Why: records created before `source_id` existed (or auto-adopted sessions)
/// have `source_id: None`, causing the guided picker to show "sessions: (none)"
/// for every project because it filters by `source_id`. The reconcile pass must
/// derive and persist the field for every live session that still lacks it.
/// Test: this test IS the coverage.
#[tokio::test]
async fn reconcile_backfills_source_id_from_workspace_git_remote() {
let tmp_ws = TempDir::new().unwrap();
let ws_path = tmp_ws.path().to_path_buf();
// Set up a real git repo with a GitHub origin remote.
let git = |args: &[&str]| {
Command::new("git")
.arg("-C")
.arg(&ws_path)
.args(args)
.output()
.map(|o| o.status.success())
.unwrap_or(false)
};
if !git(&["init"]) {
return; // no git on this runner — skip
}
git(&[
"remote",
"add",
"origin",
"git@github.com:testowner/testrepo.git",
]);
let dir = TempDir::new().unwrap();
let fake = FakeTmuxDriver::new();
// Seed seeded_names so list_sessions returns the session as live.
let tmux_name = "tmpm-backfill-test".to_string();
fake.seeded_names.lock().unwrap().push(tmux_name.clone());
let mgr = SessionManager::new(dir.path(), fake.clone())
.await
.expect("manager");
// Directly insert a record with source_id=None into the store.
let record = SessionRecord {
id: ManagedSessionId::new(),
tmux_name: tmux_name.clone(),
cwd: ws_path.clone(),
task: "test task".into(),
state: ManagedSessionState::Active,
created_at: Utc::now(),
last_activity_at: None,
workspace_path: Some(ws_path.clone()),
repo_url: None,
branch: None,
pending_decision: None,
proposed_default: None,
correlation: Default::default(),
runtime: Default::default(),
ephemeral: false,
workspace_owned: false,
source_id: None, // the field we want backfilled
claude_session_id: None,
scrollback_path: None,
last_cwd: None,
deliverable_id: None,
pane_id: None,
tmux_server: None,
injection_status: Default::default(),
worktree_owner: None,
terminal_at: None,
stop_cause: None,
kind: Default::default(),
};
mgr.store.write().await.upsert(record).await.expect("seed");
// Run reconcile — this should backfill source_id.
mgr.reconcile_on_boot(false).await.expect("reconcile");
// Assert source_id was populated from the git remote.
let updated = mgr.list().await;
let r = updated
.iter()
.find(|r| r.tmux_name == tmux_name)
.expect("record must still be present after reconcile");
assert_eq!(
r.source_id.as_deref(),
Some("testowner/testrepo"),
"reconcile must backfill source_id from the workspace git remote (#1780)"
);
}
/// Stopped-record path: reconcile must backfill `source_id` even when the
/// tmux session is gone (#1780).
///
/// Why: the reviewer noted the first test only exercises the Active path
/// (session in `seeded_names` → live). The critical user-facing case is a
/// stopped session — it must still become resumable (have a `source_id`) after
/// the daemon restarts. With the pre-loop `backfill_source_ids` call the
/// derivation now runs BEFORE the live/stopped branch, so both paths benefit.
/// What: does NOT add the session to `seeded_names`, so reconcile marks it
/// `Stopped`. Asserts `source_id` is still set and state is `Stopped`.
/// Test: this test IS the coverage.
#[tokio::test]
async fn reconcile_backfills_source_id_for_stopped_record() {
let tmp_ws = TempDir::new().unwrap();
let ws_path = tmp_ws.path().to_path_buf();
let git = |args: &[&str]| {
Command::new("git")
.arg("-C")
.arg(&ws_path)
.args(args)
.output()
.map(|o| o.status.success())
.unwrap_or(false)
};
if !git(&["init"]) {
return; // no git on this runner — skip
}
git(&[
"remote",
"add",
"origin",
"git@github.com:testowner/stopped-repo.git",
]);
let dir = TempDir::new().unwrap();
// Deliberately do NOT seed the session in seeded_names — reconcile will
// mark the record Stopped (session gone from tmux).
let fake = FakeTmuxDriver::new();
let tmux_name = "tmpm-stopped-backfill".to_string();
let mgr = SessionManager::new(dir.path(), fake.clone())
.await
.expect("manager");
let record = SessionRecord {
id: ManagedSessionId::new(),
tmux_name: tmux_name.clone(),
cwd: ws_path.clone(),
task: "stopped task".into(),
state: ManagedSessionState::Active, // starts Active, reconcile → Stopped
created_at: Utc::now(),
last_activity_at: None,
workspace_path: Some(ws_path.clone()),
repo_url: None,
branch: None,
pending_decision: None,
proposed_default: None,
correlation: Default::default(),
runtime: Default::default(),
ephemeral: false,
workspace_owned: false,
source_id: None, // the field we want backfilled
claude_session_id: None,
scrollback_path: None,
last_cwd: None,
deliverable_id: None,
pane_id: None,
tmux_server: None,
injection_status: Default::default(),
worktree_owner: None,
terminal_at: None,
stop_cause: None,
kind: Default::default(),
};
mgr.store.write().await.upsert(record).await.expect("seed");
mgr.reconcile_on_boot(false).await.expect("reconcile");
let updated = mgr.list().await;
let r = updated
.iter()
.find(|r| r.tmux_name == tmux_name)
.expect("record must still be present after reconcile");
assert_eq!(
r.state,
ManagedSessionState::Stopped,
"session not in seeded_names → must be Stopped after reconcile"
);
assert_eq!(
r.source_id.as_deref(),
Some("testowner/stopped-repo"),
"backfill must set source_id even on Stopped records (#1780)"
);
}