Skip to main content

forge_server/surface/
workers.rs

1//! `workers()`: the live workers, per project.
2
3use std::sync::Arc;
4
5use forge_primitives::SessionSlot;
6use forge_workspace::{ProjectKey, WorkerEntry, Workspace};
7
8/// The live workers a view draws under their projects.
9pub struct Workers {
10    workspace: Arc<Workspace>,
11}
12
13/// One live worker as a view reads it, named rather than a tuple.
14pub struct WorkerRef {
15    /// The project the worker belongs to.
16    pub project: ProjectKey,
17    /// The worker's label, which is also its slot's label.
18    pub label: String,
19    /// Whether the project's path was a git repo at spawn, and so
20    /// whether the worker runs in a worktree.
21    pub is_git_repo_at_spawn: bool,
22    /// Whether the worker's on-disk tag write has yet to land.
23    pub needs_tag: bool,
24}
25
26impl Workers {
27    pub(super) fn collect(workspace: Arc<Workspace>) -> Self {
28        Self { workspace }
29    }
30
31    /// The live workers registered under `project`, empty when it has
32    /// none.
33    pub fn for_project(&self, project: &ProjectKey) -> Vec<WorkerEntry> {
34        self.workspace.list_live_workers(project)
35    }
36
37    /// `slot`'s worker registration, or `None` when the slot is not a
38    /// live worker - a project lead, or a closed worker.
39    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    /// Every live worker's slot, across every project.
51    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    /// Catches worker rows that come from anywhere but the registry:
88    /// a per-project list that leaks across projects, a lookup that
89    /// answers for a lead, or a key set missing a live worker.
90    #[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}