From 025d6ddb470b327ea7930eeab37f4f19d7820733 Mon Sep 17 00:00:00 2001 From: Hare Date: Tue, 1 Sep 2026 03:16:31 +0900 Subject: [PATCH] refactor: make workspace API own workdir projections --- Cargo.lock | 1 + crates/workdir/Cargo.toml | 1 + crates/workdir/src/workspace.rs | 156 +++------------------ crates/workspace-api/src/lib.rs | 24 +++- crates/workspace-server/src/hosts.rs | 2 +- crates/workspace-server/src/lib.rs | 1 - crates/workspace-server/src/server.rs | 36 ++--- crates/workspace-server/src/workdir_api.rs | 91 ------------ 8 files changed, 57 insertions(+), 255 deletions(-) delete mode 100644 crates/workspace-server/src/workdir_api.rs diff --git a/Cargo.lock b/Cargo.lock index 8b0007c5..45c6d9d4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6590,6 +6590,7 @@ dependencies = [ "tempfile", "thiserror 2.0.18", "tokio", + "workspace-api", ] [[package]] diff --git a/crates/workdir/Cargo.toml b/crates/workdir/Cargo.toml index db2527c9..8bd23773 100644 --- a/crates/workdir/Cargo.toml +++ b/crates/workdir/Cargo.toml @@ -18,6 +18,7 @@ sha2.workspace = true tempfile.workspace = true thiserror.workspace = true tokio = { workspace = true, features = ["process", "rt", "sync", "time"] } +workspace-api = { workspace = true } [dev-dependencies] serde_json.workspace = true diff --git a/crates/workdir/src/workspace.rs b/crates/workdir/src/workspace.rs index 616eb0a9..825e5b16 100644 --- a/crates/workdir/src/workspace.rs +++ b/crates/workdir/src/workspace.rs @@ -6,7 +6,11 @@ //! [`crate::http`]. use serde::{Deserialize, Serialize}; -use std::fmt; + +pub use workspace_api::{ + WorkingDirectoryCleanupTarget, WorkingDirectoryMaterializerKind as MaterializerKind, + WorkingDirectoryOccupancy, WorkingDirectoryStatusKind, WorkingDirectorySummary, +}; /// Stable Workspace identity for a Worker hosted by a Runtime. #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] @@ -26,83 +30,6 @@ impl RuntimeWorkerRef { } } -#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "snake_case")] -pub enum MaterializerKind { - #[default] - RuntimeGitCache, - /// Legacy persisted value from the pre-cache local `git worktree` materializer. - LocalGitWorktree, -} - -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "snake_case")] -pub enum WorkingDirectoryStatusKind { - Active, - CleanupPending, - Corrupted, - NotFound, - Unknown, -} - -impl WorkingDirectoryStatusKind { - pub const fn as_str(&self) -> &'static str { - match self { - Self::Active => "active", - Self::CleanupPending => "cleanup_pending", - Self::Corrupted => "corrupted", - Self::NotFound => "not_found", - Self::Unknown => "unknown", - } - } -} - -impl fmt::Display for WorkingDirectoryStatusKind { - fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { - formatter.write_str(self.as_str()) - } -} - -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] -#[serde(deny_unknown_fields)] -pub struct WorkingDirectoryCleanupTarget { - pub kind: String, - pub working_directory_id: String, - pub repository_id: String, -} - -/// Durable Workspace occupancy projection for one Workdir. -#[derive(Clone, Debug, PartialEq, Eq, Serialize)] -pub struct WorkingDirectoryOccupancy { - #[serde(flatten)] - pub worker: RuntimeWorkerRef, - pub display_name: String, - pub linked_at: String, -} - -impl<'de> Deserialize<'de> for WorkingDirectoryOccupancy { - fn deserialize(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - #[derive(Deserialize)] - #[serde(deny_unknown_fields)] - struct Wire { - runtime_id: String, - worker_id: String, - display_name: String, - linked_at: String, - } - - let wire = Wire::deserialize(deserializer)?; - Ok(Self { - worker: RuntimeWorkerRef::new(wire.runtime_id, wire.worker_id), - display_name: wire.display_name, - linked_at: wire.linked_at, - }) - } -} - /// Immutable materialization provenance retained by Workspace inventory. #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] @@ -139,67 +66,6 @@ pub struct WorkingDirectoryCurrentObservation { pub occupied_by: Option, } -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] -#[serde(deny_unknown_fields)] -pub struct WorkingDirectorySummary { - pub working_directory_id: String, - pub repository_id: String, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub creation_selector: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub creation_ref: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub creation_tree: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub current_selector: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub current_ref: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub current_tree: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub observed_at_epoch_seconds: Option, - pub materializer_kind: MaterializerKind, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub cleanup_target: Option, - pub status: WorkingDirectoryStatusKind, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub cleanliness: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub primary_worker_id: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub occupied_by: Option, -} - -impl WorkingDirectorySummary { - /// Workspace-managed inventory rows carry explicit cleanup authority. - pub fn is_workspace_managed(&self) -> bool { - self.cleanup_target.is_some() - } - - pub fn provenance(&self) -> WorkingDirectoryProvenance { - WorkingDirectoryProvenance { - creation_selector: self.creation_selector.clone(), - creation_ref: self.creation_ref.clone(), - creation_tree: self.creation_tree.clone(), - materializer_kind: self.materializer_kind.clone(), - cleanup_target: self.cleanup_target.clone(), - } - } - - pub fn current_observation(&self) -> WorkingDirectoryCurrentObservation { - WorkingDirectoryCurrentObservation { - current_selector: self.current_selector.clone(), - current_ref: self.current_ref.clone(), - current_tree: self.current_tree.clone(), - observed_at_epoch_seconds: self.observed_at_epoch_seconds, - status: self.status.clone(), - cleanliness: self.cleanliness.clone(), - primary_worker_id: self.primary_worker_id.clone(), - occupied_by: self.occupied_by.clone(), - } - } -} - #[cfg(test)] mod tests { use super::*; @@ -220,6 +86,18 @@ mod tests { assert_eq!(serde_json::to_value(status).unwrap(), expected); } } + + #[test] + fn workspace_workdir_projection_reexports_workspace_api_authority() { + assert_eq!( + std::any::TypeId::of::(), + std::any::TypeId::of::() + ); + assert_eq!( + std::any::TypeId::of::(), + std::any::TypeId::of::() + ); + } } #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] diff --git a/crates/workspace-api/src/lib.rs b/crates/workspace-api/src/lib.rs index 3dd8aba3..ec937882 100644 --- a/crates/workspace-api/src/lib.rs +++ b/crates/workspace-api/src/lib.rs @@ -113,10 +113,11 @@ pub struct Diagnostic { /// /// The value identifies stable materialization provenance without exposing a /// provider path, Runtime handle, or session identity. -#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] +#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)] #[cfg_attr(feature = "typescript", derive(ts_rs::TS))] #[serde(rename_all = "snake_case")] pub enum WorkingDirectoryMaterializerKind { + #[default] RuntimeGitCache, LocalGitWorktree, } @@ -132,15 +133,21 @@ pub enum WorkingDirectoryStatusKind { Unknown, } -impl std::fmt::Display for WorkingDirectoryStatusKind { - fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - formatter.write_str(match self { +impl WorkingDirectoryStatusKind { + pub const fn as_str(&self) -> &'static str { + match self { Self::Active => "active", Self::CleanupPending => "cleanup_pending", Self::Corrupted => "corrupted", Self::NotFound => "not_found", Self::Unknown => "unknown", - }) + } + } +} + +impl std::fmt::Display for WorkingDirectoryStatusKind { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str(self.as_str()) } } @@ -199,6 +206,13 @@ pub struct WorkingDirectorySummary { pub occupied_by: Option, } +impl WorkingDirectorySummary { + /// Workspace-managed inventory rows carry explicit cleanup authority. + pub fn is_workspace_managed(&self) -> bool { + self.cleanup_target.is_some() + } +} + /// Browser/Rust-client Workdir materialization request. /// /// `runtime_id = None` requests Workspace default Runtime resolution and diff --git a/crates/workspace-server/src/hosts.rs b/crates/workspace-server/src/hosts.rs index 70ea6e7d..3e06d842 100644 --- a/crates/workspace-server/src/hosts.rs +++ b/crates/workspace-server/src/hosts.rs @@ -341,7 +341,7 @@ pub(crate) fn workspace_worker_summary( can_stop: summary.capabilities.can_stop, can_spawn_followup: summary.capabilities.can_spawn_followup, }, - working_directory: summary.working_directory.map(crate::workdir_api::summary), + working_directory: summary.working_directory, diagnostics: summary.diagnostics.into_iter().map(Into::into).collect(), } } diff --git a/crates/workspace-server/src/lib.rs b/crates/workspace-server/src/lib.rs index f6e1a4a7..3c17be14 100644 --- a/crates/workspace-server/src/lib.rs +++ b/crates/workspace-server/src/lib.rs @@ -29,7 +29,6 @@ pub mod runtime_subscription; pub mod server; pub mod skills; pub mod store; -mod workdir_api; pub mod workdir_create_operations; pub mod worker_source; pub mod workspace_catalog; diff --git a/crates/workspace-server/src/server.rs b/crates/workspace-server/src/server.rs index aaa1949e..171e6c49 100644 --- a/crates/workspace-server/src/server.rs +++ b/crates/workspace-server/src/server.rs @@ -8839,7 +8839,7 @@ async fn scoped_list_runtime_working_directories( let (items, diagnostics) = runtime_working_directory_summaries(&api, &path.runtime_id)?; Ok(Json(BrowserWorkingDirectoryListResponse { workspace_id: api.config.workspace_id.clone(), - items: items.into_iter().map(crate::workdir_api::summary).collect(), + items, diagnostics: working_directory_diagnostics(diagnostics), })) } @@ -8882,7 +8882,7 @@ async fn scoped_list_working_directories( let items = working_directory_summaries(&api)?; Ok(Json(BrowserWorkingDirectoryListResponse { workspace_id: api.config.workspace_id.clone(), - items: items.into_iter().map(crate::workdir_api::summary).collect(), + items, diagnostics: Vec::new(), })) } @@ -9339,7 +9339,7 @@ async fn create_workspace_working_directory( Json(BrowserWorkingDirectoryCreateResponse { workspace_id: workspace_id.to_string(), runtime_id: reserved.resolved_runtime_id, - item: crate::workdir_api::summary(summary), + item: summary, diagnostics: working_directory_diagnostics(result.diagnostics), }), )) @@ -9362,7 +9362,7 @@ fn working_directory_detail_for_runtime( return Ok(Json(BrowserWorkingDirectoryDetailResponse { workspace_id: api.config.workspace_id.clone(), runtime_id: runtime_id.to_string(), - item: crate::workdir_api::summary(summary), + item: summary, diagnostics: working_directory_diagnostics(result.diagnostics), })); } @@ -9373,9 +9373,7 @@ fn working_directory_detail_for_runtime( return Ok(Json(BrowserWorkingDirectoryDetailResponse { workspace_id: api.config.workspace_id.clone(), runtime_id: runtime_id.to_string(), - item: crate::workdir_api::summary(projected_workdir_summary_from_record( - &api, &record, - )?), + item: projected_workdir_summary_from_record(&api, &record)?, diagnostics: working_directory_diagnostics(result.diagnostics), })); } @@ -9434,7 +9432,7 @@ fn cleanup_working_directory_for_runtime( Ok(Json(BrowserWorkingDirectoryDetailResponse { workspace_id: api.config.workspace_id.clone(), runtime_id: runtime_id.to_string(), - item: crate::workdir_api::summary(summary), + item: summary, diagnostics: working_directory_diagnostics(result.diagnostics), })) } @@ -14152,7 +14150,8 @@ fn merge_worker_registry_projection( .map(|workdir| { let mut workdir_summary = workdir_summary_from_record(workdir); workdir_summary.occupied_by = Some(WorkingDirectoryOccupancy { - worker: record.worker.clone(), + runtime_id: record.worker.runtime_id.clone(), + worker_id: record.worker.worker_id.clone(), display_name: record.display_name.clone(), linked_at: link.linked_at.clone(), }); @@ -14501,7 +14500,8 @@ fn apply_workdir_occupancy_projection( })?; summary.primary_worker_id = None; summary.occupied_by = Some(WorkingDirectoryOccupancy { - worker: link.worker.clone(), + runtime_id: link.worker.runtime_id.clone(), + worker_id: link.worker.worker_id.clone(), display_name: worker.display_name, linked_at: link.linked_at.clone(), }); @@ -15955,7 +15955,8 @@ mod tests { assert_eq!(working_directory.current_selector, None); assert_eq!(working_directory.current_ref.as_deref(), Some("fedcba")); let occupied_by = working_directory.occupied_by.as_ref().unwrap(); - assert_eq!(occupied_by.worker, RuntimeWorkerRef::new("embedded", "1")); + assert_eq!(occupied_by.runtime_id, "embedded"); + assert_eq!(occupied_by.worker_id, "1"); assert!(working_directory.primary_worker_id.is_none()); let occupancy = serde_json::to_value(occupied_by).unwrap(); assert_eq!(occupancy["runtime_id"], "embedded"); @@ -17160,10 +17161,8 @@ mod tests { .find(|summary| summary.working_directory_id == "managed") .unwrap(); let occupied_by = managed.occupied_by.as_ref().unwrap(); - assert_eq!( - occupied_by.worker, - RuntimeWorkerRef::new(EMBEDDED_WORKER_RUNTIME_ID, "7") - ); + assert_eq!(occupied_by.runtime_id, EMBEDDED_WORKER_RUNTIME_ID); + assert_eq!(occupied_by.worker_id, "7"); assert_eq!(occupied_by.display_name, "Worker Seven"); assert_eq!(occupied_by.linked_at, "3"); @@ -25499,7 +25498,7 @@ VALUES ('0192f0e8-4d84-7d6e-a000-000000000001', 'ticket', 3); fn workspace_workdir_response_serializes_shared_occupied_contract() { let response = BrowserWorkingDirectoryListResponse { workspace_id: TEST_WORKSPACE_ID.to_string(), - items: vec![crate::workdir_api::summary(WorkingDirectorySummary { + items: vec![WorkingDirectorySummary { working_directory_id: "wd-1".to_string(), repository_id: "main".to_string(), creation_selector: None, @@ -25515,11 +25514,12 @@ VALUES ('0192f0e8-4d84-7d6e-a000-000000000001', 'ticket', 3); cleanliness: Some("clean".to_string()), primary_worker_id: None, occupied_by: Some(WorkingDirectoryOccupancy { - worker: RuntimeWorkerRef::new("arcadia", "worker-opaque-64"), + runtime_id: "arcadia".to_string(), + worker_id: "worker-opaque-64".to_string(), display_name: "Coder".to_string(), linked_at: "2026-08-12T00:00:00Z".to_string(), }), - })], + }], diagnostics: vec![], }; diff --git a/crates/workspace-server/src/workdir_api.rs b/crates/workspace-server/src/workdir_api.rs deleted file mode 100644 index c1a8eb62..00000000 --- a/crates/workspace-server/src/workdir_api.rs +++ /dev/null @@ -1,91 +0,0 @@ -use workdir::workspace::{MaterializerKind, WorkingDirectoryStatusKind}; -use workspace_api::{ - WorkingDirectoryCleanupTarget, WorkingDirectoryMaterializerKind, WorkingDirectoryOccupancy, - WorkingDirectoryStatusKind as ApiWorkingDirectoryStatusKind, WorkingDirectorySummary, -}; - -pub(crate) fn summary( - source: workdir::workspace::WorkingDirectorySummary, -) -> WorkingDirectorySummary { - WorkingDirectorySummary { - working_directory_id: source.working_directory_id, - repository_id: source.repository_id, - creation_selector: source.creation_selector, - creation_ref: source.creation_ref, - creation_tree: source.creation_tree, - current_selector: source.current_selector, - current_ref: source.current_ref, - current_tree: source.current_tree, - observed_at_epoch_seconds: source.observed_at_epoch_seconds, - materializer_kind: match source.materializer_kind { - MaterializerKind::RuntimeGitCache => WorkingDirectoryMaterializerKind::RuntimeGitCache, - MaterializerKind::LocalGitWorktree => { - WorkingDirectoryMaterializerKind::LocalGitWorktree - } - }, - cleanup_target: source - .cleanup_target - .map(|target| WorkingDirectoryCleanupTarget { - kind: target.kind, - working_directory_id: target.working_directory_id, - repository_id: target.repository_id, - }), - status: match source.status { - WorkingDirectoryStatusKind::Active => ApiWorkingDirectoryStatusKind::Active, - WorkingDirectoryStatusKind::CleanupPending => { - ApiWorkingDirectoryStatusKind::CleanupPending - } - WorkingDirectoryStatusKind::Corrupted => ApiWorkingDirectoryStatusKind::Corrupted, - WorkingDirectoryStatusKind::NotFound => ApiWorkingDirectoryStatusKind::NotFound, - WorkingDirectoryStatusKind::Unknown => ApiWorkingDirectoryStatusKind::Unknown, - }, - cleanliness: source.cleanliness, - primary_worker_id: source.primary_worker_id, - occupied_by: source - .occupied_by - .map(|occupancy| WorkingDirectoryOccupancy { - runtime_id: occupancy.worker.runtime_id, - worker_id: occupancy.worker.worker_id, - display_name: occupancy.display_name, - linked_at: occupancy.linked_at, - }), - } -} - -#[cfg(test)] -mod tests { - use super::*; - use workdir::workspace::{ - RuntimeWorkerRef, WorkingDirectoryOccupancy as DomainOccupancy, - WorkingDirectorySummary as DomainSummary, - }; - - #[test] - fn conversion_preserves_public_occupancy_subject() { - let converted = summary(DomainSummary { - working_directory_id: "workdir-1".into(), - repository_id: "main".into(), - creation_selector: None, - creation_ref: None, - creation_tree: None, - current_selector: None, - current_ref: None, - current_tree: None, - observed_at_epoch_seconds: None, - materializer_kind: MaterializerKind::RuntimeGitCache, - cleanup_target: None, - status: WorkingDirectoryStatusKind::Active, - cleanliness: None, - primary_worker_id: None, - occupied_by: Some(DomainOccupancy { - worker: RuntimeWorkerRef::new("arcadia", "worker-1"), - display_name: "Coder".into(), - linked_at: "2026-01-01T00:00:00Z".into(), - }), - }); - - let occupancy = converted.occupied_by.expect("occupancy"); - assert_eq!(occupancy.runtime_id, "arcadia"); - assert_eq!(occupancy.worker_id, "worker-1"); - } -}