1use std::path::PathBuf;
12
13use crate::SessionSlot;
14use crate::target::ProjectKey;
15use crate::views::ProjectView;
16use crate::workspace::Workspace;
17
18#[derive(Debug)]
20pub(crate) struct CallerContext {
21 pub project_key: ProjectKey,
23 pub project_name: String,
25 pub project_org: String,
26 pub project_path: PathBuf,
27 pub lead: SessionSlot,
31 pub lead_running: bool,
33 pub is_lead: bool,
35 pub worker_label: Option<String>,
38}
39
40pub(crate) fn caller_context(ws: &Workspace, caller: &SessionSlot) -> Option<CallerContext> {
45 ws.list_projects().into_iter().find_map(|view| caller_context_in_view(ws, &view, caller))
46}
47
48fn caller_context_in_view(
52 ws: &Workspace,
53 view: &ProjectView,
54 caller: &SessionSlot,
55) -> Option<CallerContext> {
56 if caller.org() != view.org || caller.project() != view.name {
57 return None;
58 }
59 let live = ws.list_live_workers(&view.key);
60 let worker_label = live.iter().find(|w| w.slot == *caller).map(|w| w.label.clone());
61 let is_lead = worker_label.is_none() && caller.is_lead();
62 let lead = SessionSlot::lead(&view.org, &view.name);
63 let lead_running = ws.session_is_pooled(&lead);
64 Some(CallerContext {
65 project_key: view.key.clone(),
66 project_name: view.name.clone(),
67 project_org: view.org.clone(),
68 project_path: view.path.clone(),
69 lead,
70 lead_running,
71 is_lead,
72 worker_label,
73 })
74}
75
76#[cfg(all(test, feature = "test-helpers"))]
77mod tests {
78 use super::*;
79 use crate::WorkerEntry;
80 use forge_primitives::WorkerLiveness;
81 use std::time::SystemTime;
82
83 fn worker_entry(slot: SessionSlot) -> WorkerEntry {
84 WorkerEntry {
85 label: "reviewer".into(),
86 charter: "review the diff".into(),
87 slot,
88 session_id: None,
89 status: WorkerLiveness::Running,
90 spawned_at: SystemTime::UNIX_EPOCH,
91 spawned_by: SessionSlot::lead("me", "myproj"),
92 needs_tag: false,
93 is_git_repo_at_spawn: false,
94 cwd: None,
95 diagnostic: None,
96 kick: None,
97 }
98 }
99
100 fn fixture() -> (std::sync::Arc<Workspace>, ProjectView, SessionSlot, SessionSlot) {
101 let (ws, _rx) = Workspace::testing_stub();
102 let key = ProjectKey::new("myproj".to_owned());
103 let lead = SessionSlot::lead("me", "myproj");
104 let worker = SessionSlot::worker("me", "myproj", "reviewer");
105 let view = ProjectView::new_for_test_with_org(
106 key.clone(),
107 "myproj",
108 "/tmp/myproj",
109 "me",
110 Vec::new(),
111 Vec::new(),
112 vec![],
113 );
114 ws.insert_live_worker(&key, worker_entry(worker.clone()));
115 (ws, view, lead, worker)
116 }
117
118 #[test]
119 fn caller_context_resolves_worker_as_non_lead() {
120 let (ws, view, lead, worker) = fixture();
121 let cx = caller_context_in_view(&ws, &view, &worker)
122 .expect("worker caller resolves to its project");
123 assert_eq!(cx.project_name, "myproj");
124 assert_eq!(cx.project_org, "me");
125 assert_eq!(cx.project_key.as_str(), "myproj");
126 assert!(!cx.is_lead, "worker is not the lead");
127 assert_eq!(cx.worker_label.as_deref(), Some("reviewer"), "a live worker carries its label");
128 assert_eq!(cx.lead, lead, "the project's lead slot is named by the triple");
129 assert!(!cx.lead_running, "no lead is pooled in this fixture");
130 }
131
132 #[test]
133 fn caller_context_resolves_lead_as_lead() {
134 let (ws, view, lead, _) = fixture();
135 let cx =
136 caller_context_in_view(&ws, &view, &lead).expect("lead caller resolves to its project");
137 assert!(cx.is_lead, "lead caller flagged as lead");
138 assert_eq!(cx.worker_label, None, "a lead has no worker label");
139 assert_eq!(cx.lead, lead);
140 }
141
142 #[test]
145 fn caller_context_returns_none_for_an_unrelated_caller() {
146 let (ws, view, _, _) = fixture();
147 let elsewhere = SessionSlot::worker("someone-else", "otherproj", "reviewer");
148 assert!(
149 caller_context_in_view(&ws, &view, &elsewhere).is_none(),
150 "a caller under another project must return None",
151 );
152 }
153
154 #[test]
158 fn caller_context_names_the_lead_slot_with_no_catalog_rows() {
159 let (ws, _rx) = Workspace::testing_stub();
160 let key = ProjectKey::new("myproj".to_owned());
161 let worker = SessionSlot::worker("me", "myproj", "reviewer");
162 ws.insert_live_worker(&key, worker_entry(worker.clone()));
163 let view = ProjectView::new_for_test_with_org(
164 key,
165 "myproj",
166 "/tmp/myproj",
167 "me",
168 Vec::new(),
169 Vec::new(),
170 vec![],
171 );
172
173 let cx = caller_context_in_view(&ws, &view, &worker).expect("worker resolves");
174 assert_eq!(cx.lead, SessionSlot::lead("me", "myproj"));
175 }
176}