forge_server/surface/
workers.rs1use std::sync::Arc;
4
5use forge_primitives::SessionSlot;
6use forge_workspace::{ProjectKey, WorkerEntry, Workspace};
7
8pub struct Workers {
10 workspace: Arc<Workspace>,
11}
12
13pub struct WorkerRef {
15 pub project: ProjectKey,
17 pub label: String,
19 pub is_git_repo_at_spawn: bool,
22 pub needs_tag: bool,
24}
25
26impl Workers {
27 pub(super) fn collect(workspace: Arc<Workspace>) -> Self {
28 Self { workspace }
29 }
30
31 pub fn for_project(&self, project: &ProjectKey) -> Vec<WorkerEntry> {
34 self.workspace.list_live_workers(project)
35 }
36
37 pub fn lookup(&self, slot: &SessionSlot) -> Option<WorkerRef> {
40 self.workspace.worker_lookup_for_session(slot).map(
41 |(project, label, is_git_repo_at_spawn, needs_tag, _)| WorkerRef {
42 project,
43 label,
44 is_git_repo_at_spawn,
45 needs_tag,
46 },
47 )
48 }
49
50 pub fn all_keys(&self) -> Vec<SessionSlot> {
52 self.workspace.all_live_worker_session_keys()
53 }
54}
55
56#[cfg(test)]
57mod tests {
58 use std::sync::Arc;
59
60 use forge_primitives::{SessionSlot, WorkerLiveness};
61 use forge_workspace::{WorkerEntry, Workspace};
62
63 use crate::surface::ViewSurface;
64
65 fn stub_workspace() -> Arc<Workspace> {
66 let (workspace, _updates) = Workspace::testing_stub();
67 workspace
68 }
69
70 fn worker_row(project: &str, label: &str) -> WorkerEntry {
71 WorkerEntry {
72 label: label.to_owned(),
73 charter: format!("charter for {label}"),
74 slot: SessionSlot::worker("TestOrg", project, label),
75 session_id: None,
76 status: WorkerLiveness::Running,
77 spawned_at: std::time::SystemTime::UNIX_EPOCH,
78 spawned_by: SessionSlot::lead("TestOrg", project),
79 needs_tag: false,
80 is_git_repo_at_spawn: false,
81 cwd: None,
82 diagnostic: None,
83 kick: None,
84 }
85 }
86
87 #[test]
91 fn worker_reads_agree_with_the_workspace() {
92 let workspace = stub_workspace();
93 workspace.seed_test_project("forge", "/tmp/forge-workers");
94 workspace.seed_test_project("other", "/tmp/forge-workers-other");
95 let surface = ViewSurface::new(Arc::clone(&workspace));
96 let roster = surface.roster();
97 let project = roster.project_named("forge").expect("seeded project").key.clone();
98 let elsewhere = roster.project_named("other").expect("seeded project").key.clone();
99 workspace.insert_live_worker(&project, worker_row("forge", "probe-a"));
100
101 let workers = surface.workers();
102
103 assert_eq!(
104 workers.for_project(&project).len(),
105 workspace.list_live_workers(&project).len(),
106 "for_project must return the project's own live workers"
107 );
108 assert_eq!(
109 workers.for_project(&project).first().map(|w| w.label.clone()),
110 Some("probe-a".to_owned()),
111 "the seeded worker must be the row for_project returns"
112 );
113 assert!(
114 workers.for_project(&elsewhere).is_empty(),
115 "a project with no workers must list none"
116 );
117
118 let seeded = workspace.list_live_workers(&project).first().cloned().expect("seeded row");
119 let lookup = workers.lookup(&seeded.slot).expect("a live worker is found by its slot");
120 assert_eq!(lookup.label, seeded.label, "lookup must answer with the worker's own label");
121 assert_eq!(lookup.project, project, "lookup must answer with the worker's own project");
122 assert!(
123 workers.lookup(&SessionSlot::lead("TestOrg", "forge")).is_none(),
124 "a lead slot must not resolve as a worker"
125 );
126 assert_eq!(
127 workers.all_keys(),
128 workspace.all_live_worker_session_keys(),
129 "all_keys must be the registry's own key set"
130 );
131 assert!(
132 workers.all_keys().contains(&seeded.slot),
133 "all_keys must carry the seeded worker's slot"
134 );
135 }
136}