Merge ticket 00001KZVPX841 workspace Workdir contract
This commit is contained in:
Generated
+1
@@ -539,6 +539,7 @@ dependencies = [
|
|||||||
"tokio",
|
"tokio",
|
||||||
"tokio-tungstenite 0.29.0",
|
"tokio-tungstenite 0.29.0",
|
||||||
"uuid",
|
"uuid",
|
||||||
|
"workdir",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ thiserror = { workspace = true }
|
|||||||
tokio = { workspace = true, features = ["rt", "macros", "net", "io-util", "sync", "time", "process", "fs"] }
|
tokio = { workspace = true, features = ["rt", "macros", "net", "io-util", "sync", "time", "process", "fs"] }
|
||||||
tokio-tungstenite = { workspace = true }
|
tokio-tungstenite = { workspace = true }
|
||||||
uuid = { workspace = true }
|
uuid = { workspace = true }
|
||||||
|
workdir = { workspace = true }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
tempfile = { workspace = true }
|
tempfile = { workspace = true }
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ use std::fmt;
|
|||||||
use tokio::sync::mpsc;
|
use tokio::sync::mpsc;
|
||||||
use tokio_tungstenite::connect_async;
|
use tokio_tungstenite::connect_async;
|
||||||
use tokio_tungstenite::tungstenite::Message as TungsteniteMessage;
|
use tokio_tungstenite::tungstenite::Message as TungsteniteMessage;
|
||||||
|
pub use workdir::workspace::WorkingDirectorySummary as BackendWorkingDirectorySummary;
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub struct BackendRuntimeTarget {
|
pub struct BackendRuntimeTarget {
|
||||||
@@ -98,46 +99,6 @@ pub struct BackendWorkerCapabilitySummary {
|
|||||||
pub can_spawn_followup: bool,
|
pub can_spawn_followup: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
|
|
||||||
pub struct BackendWorkingDirectoryCleanupTarget {
|
|
||||||
pub kind: String,
|
|
||||||
pub working_directory_id: String,
|
|
||||||
pub repository_id: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
|
|
||||||
pub struct BackendWorkingDirectoryOccupancy {
|
|
||||||
pub runtime_id: String,
|
|
||||||
pub runtime_worker_id: u64,
|
|
||||||
pub worker_id: String,
|
|
||||||
pub display_name: String,
|
|
||||||
pub linked_at: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
|
|
||||||
pub struct BackendWorkingDirectorySummary {
|
|
||||||
pub working_directory_id: String,
|
|
||||||
pub repository_id: String,
|
|
||||||
#[serde(default)]
|
|
||||||
pub creation_selector: Option<String>,
|
|
||||||
#[serde(default)]
|
|
||||||
pub creation_ref: Option<String>,
|
|
||||||
#[serde(default)]
|
|
||||||
pub current_selector: Option<String>,
|
|
||||||
#[serde(default)]
|
|
||||||
pub current_ref: Option<String>,
|
|
||||||
pub materializer_kind: String,
|
|
||||||
#[serde(default)]
|
|
||||||
pub cleanup_target: Option<BackendWorkingDirectoryCleanupTarget>,
|
|
||||||
pub status: String,
|
|
||||||
#[serde(default)]
|
|
||||||
pub cleanliness: Option<String>,
|
|
||||||
#[serde(default)]
|
|
||||||
pub primary_worker_id: Option<u64>,
|
|
||||||
#[serde(default)]
|
|
||||||
pub occupied_by: Option<BackendWorkingDirectoryOccupancy>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
|
#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
|
||||||
pub struct BackendWorkerSummary {
|
pub struct BackendWorkerSummary {
|
||||||
pub runtime_id: String,
|
pub runtime_id: String,
|
||||||
@@ -618,6 +579,46 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn backend_worker_summary_decodes_current_occupied_workdir_contract() {
|
||||||
|
let payload = serde_json::json!({
|
||||||
|
"runtime_id": "arcadia",
|
||||||
|
"worker_id": "worker-opaque-64",
|
||||||
|
"host_id": "host",
|
||||||
|
"display_name": "Coder",
|
||||||
|
"label": "Coder",
|
||||||
|
"workspace": {"visibility": "workspace", "identity": "workspace"},
|
||||||
|
"state": "idle",
|
||||||
|
"implementation": {"kind": "worker", "display_hint": "Coder"},
|
||||||
|
"capabilities": {"can_stop": true, "can_spawn_followup": false},
|
||||||
|
"working_directory": {
|
||||||
|
"working_directory_id": "wd-1",
|
||||||
|
"repository_id": "main",
|
||||||
|
"materializer_kind": "local_git_worktree",
|
||||||
|
"status": "active",
|
||||||
|
"occupied_by": {
|
||||||
|
"runtime_id": "arcadia",
|
||||||
|
"worker_id": "worker-opaque-64",
|
||||||
|
"display_name": "Coder",
|
||||||
|
"linked_at": "2026-08-12T00:00:00Z"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let worker: BackendWorkerSummary = serde_json::from_value(payload.clone()).unwrap();
|
||||||
|
let occupied_by = worker
|
||||||
|
.working_directory
|
||||||
|
.unwrap()
|
||||||
|
.occupied_by
|
||||||
|
.expect("occupied Workdir");
|
||||||
|
assert_eq!(occupied_by.worker.runtime_id, "arcadia");
|
||||||
|
assert_eq!(occupied_by.worker.worker_id, "worker-opaque-64");
|
||||||
|
|
||||||
|
let mut stale = payload;
|
||||||
|
stale["working_directory"]["occupied_by"]["runtime_worker_id"] = serde_json::json!(64);
|
||||||
|
assert!(serde_json::from_value::<BackendWorkerSummary>(stale).is_err());
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn workers_path_can_be_workspace_scoped_for_status_queries() {
|
fn workers_path_can_be_workspace_scoped_for_status_queries() {
|
||||||
let path = backend_runtime_workers_path(Some("team main"), "runtime/one");
|
let path = backend_runtime_workers_path(Some("team main"), "runtime/one");
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
pub mod http;
|
pub mod http;
|
||||||
mod local;
|
mod local;
|
||||||
mod operation;
|
mod operation;
|
||||||
|
pub mod workspace;
|
||||||
|
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|||||||
@@ -0,0 +1,280 @@
|
|||||||
|
//! Shared Workspace-facing Workdir inventory transport contracts.
|
||||||
|
//!
|
||||||
|
//! These projections describe Workspace inventory and durable occupancy. They
|
||||||
|
//! intentionally do not expose provider/session handles, host paths, Runtime
|
||||||
|
//! URLs, or credentials. Runtime-local Workdir operation contracts live in
|
||||||
|
//! [`crate::http`].
|
||||||
|
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
/// Stable Workspace identity for a Worker hosted by a Runtime.
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
|
||||||
|
#[serde(deny_unknown_fields)]
|
||||||
|
pub struct RuntimeWorkerRef {
|
||||||
|
pub runtime_id: String,
|
||||||
|
/// Runtime-owned opaque Worker id. Consumers must not assume a numeric id.
|
||||||
|
pub worker_id: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RuntimeWorkerRef {
|
||||||
|
pub fn new(runtime_id: impl Into<String>, worker_id: impl Into<String>) -> Self {
|
||||||
|
Self {
|
||||||
|
runtime_id: runtime_id.into(),
|
||||||
|
worker_id: worker_id.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
pub enum MaterializerKind {
|
||||||
|
#[default]
|
||||||
|
LocalGitWorktree,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
pub enum WorkingDirectoryStatusKind {
|
||||||
|
Active,
|
||||||
|
CleanupPending,
|
||||||
|
Corrupted,
|
||||||
|
NotFound,
|
||||||
|
Unknown,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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<D>(deserializer: D) -> Result<Self, D::Error>
|
||||||
|
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)]
|
||||||
|
pub struct WorkingDirectoryProvenance {
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub creation_selector: Option<String>,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub creation_ref: Option<String>,
|
||||||
|
pub materializer_kind: MaterializerKind,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub cleanup_target: Option<WorkingDirectoryCleanupTarget>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Latest provider-neutral observation attached to Workspace inventory.
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
#[serde(deny_unknown_fields)]
|
||||||
|
pub struct WorkingDirectoryCurrentObservation {
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub current_selector: Option<String>,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub current_ref: Option<String>,
|
||||||
|
pub status: WorkingDirectoryStatusKind,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub cleanliness: Option<String>,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub primary_worker_id: Option<u64>,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub occupied_by: Option<WorkingDirectoryOccupancy>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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<String>,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub creation_ref: Option<String>,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub current_selector: Option<String>,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub current_ref: Option<String>,
|
||||||
|
pub materializer_kind: MaterializerKind,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub cleanup_target: Option<WorkingDirectoryCleanupTarget>,
|
||||||
|
pub status: WorkingDirectoryStatusKind,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub cleanliness: Option<String>,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub primary_worker_id: Option<u64>,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub occupied_by: Option<WorkingDirectoryOccupancy>,
|
||||||
|
}
|
||||||
|
|
||||||
|
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(),
|
||||||
|
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(),
|
||||||
|
status: self.status.clone(),
|
||||||
|
cleanliness: self.cleanliness.clone(),
|
||||||
|
primary_worker_id: self.primary_worker_id,
|
||||||
|
occupied_by: self.occupied_by.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
pub enum WorkingDirectoryDiagnosticSeverity {
|
||||||
|
Info,
|
||||||
|
Warning,
|
||||||
|
Error,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
#[serde(deny_unknown_fields)]
|
||||||
|
pub struct WorkingDirectoryDiagnostic {
|
||||||
|
pub code: String,
|
||||||
|
pub severity: WorkingDirectoryDiagnosticSeverity,
|
||||||
|
pub message: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
#[serde(deny_unknown_fields)]
|
||||||
|
pub struct WorkingDirectoryListResponse {
|
||||||
|
pub workspace_id: String,
|
||||||
|
pub items: Vec<WorkingDirectorySummary>,
|
||||||
|
pub diagnostics: Vec<WorkingDirectoryDiagnostic>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
#[serde(deny_unknown_fields)]
|
||||||
|
pub struct WorkingDirectoryDetailResponse {
|
||||||
|
pub workspace_id: String,
|
||||||
|
pub item: WorkingDirectorySummary,
|
||||||
|
pub diagnostics: Vec<WorkingDirectoryDiagnostic>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn occupied_and_free_list_response_round_trips() {
|
||||||
|
let response = WorkingDirectoryListResponse {
|
||||||
|
workspace_id: "workspace".to_string(),
|
||||||
|
items: vec![
|
||||||
|
WorkingDirectorySummary {
|
||||||
|
working_directory_id: "occupied".to_string(),
|
||||||
|
repository_id: "repo".to_string(),
|
||||||
|
creation_selector: Some("develop".to_string()),
|
||||||
|
creation_ref: Some("abc123".to_string()),
|
||||||
|
current_selector: Some("work/ticket".to_string()),
|
||||||
|
current_ref: Some("def456".to_string()),
|
||||||
|
materializer_kind: MaterializerKind::LocalGitWorktree,
|
||||||
|
cleanup_target: Some(WorkingDirectoryCleanupTarget {
|
||||||
|
kind: "git_worktree".to_string(),
|
||||||
|
working_directory_id: "occupied".to_string(),
|
||||||
|
repository_id: "repo".to_string(),
|
||||||
|
}),
|
||||||
|
status: WorkingDirectoryStatusKind::Active,
|
||||||
|
cleanliness: Some("clean".to_string()),
|
||||||
|
primary_worker_id: None,
|
||||||
|
occupied_by: Some(WorkingDirectoryOccupancy {
|
||||||
|
worker: RuntimeWorkerRef::new("arcadia", "worker-opaque-64"),
|
||||||
|
display_name: "Coder".to_string(),
|
||||||
|
linked_at: "2026-08-12T00:00:00Z".to_string(),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
WorkingDirectorySummary {
|
||||||
|
working_directory_id: "free".to_string(),
|
||||||
|
repository_id: "repo".to_string(),
|
||||||
|
creation_selector: None,
|
||||||
|
creation_ref: None,
|
||||||
|
current_selector: None,
|
||||||
|
current_ref: Some("987fed".to_string()),
|
||||||
|
materializer_kind: MaterializerKind::LocalGitWorktree,
|
||||||
|
cleanup_target: None,
|
||||||
|
status: WorkingDirectoryStatusKind::Active,
|
||||||
|
cleanliness: Some("unknown".to_string()),
|
||||||
|
primary_worker_id: None,
|
||||||
|
occupied_by: None,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
diagnostics: vec![WorkingDirectoryDiagnostic {
|
||||||
|
code: "observed".to_string(),
|
||||||
|
severity: WorkingDirectoryDiagnosticSeverity::Info,
|
||||||
|
message: "inventory observed".to_string(),
|
||||||
|
}],
|
||||||
|
};
|
||||||
|
|
||||||
|
let encoded = serde_json::to_value(&response).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
encoded["items"][0]["occupied_by"]["worker_id"],
|
||||||
|
"worker-opaque-64"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
encoded["items"][0]["occupied_by"]
|
||||||
|
.get("runtime_worker_id")
|
||||||
|
.is_none()
|
||||||
|
);
|
||||||
|
assert!(encoded["items"][1].get("occupied_by").is_none());
|
||||||
|
|
||||||
|
let mut stale = encoded.clone();
|
||||||
|
stale["items"][0]["occupied_by"]["runtime_worker_id"] = serde_json::json!(64);
|
||||||
|
assert!(serde_json::from_value::<WorkingDirectoryListResponse>(stale).is_err());
|
||||||
|
|
||||||
|
let decoded: WorkingDirectoryListResponse = serde_json::from_value(encoded).unwrap();
|
||||||
|
assert_eq!(decoded, response);
|
||||||
|
|
||||||
|
let detail = WorkingDirectoryDetailResponse {
|
||||||
|
workspace_id: decoded.workspace_id.clone(),
|
||||||
|
item: decoded.items[0].clone(),
|
||||||
|
diagnostics: decoded.diagnostics.clone(),
|
||||||
|
};
|
||||||
|
let encoded = serde_json::to_value(&detail).unwrap();
|
||||||
|
let decoded: WorkingDirectoryDetailResponse = serde_json::from_value(encoded).unwrap();
|
||||||
|
assert_eq!(decoded, detail);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -92,12 +92,11 @@ pub struct WorkingDirectoryRepository {
|
|||||||
pub selector: Option<RepositorySelector>,
|
pub selector: Option<RepositorySelector>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
|
pub use workdir::workspace::{
|
||||||
#[serde(rename_all = "snake_case")]
|
MaterializerKind, WorkingDirectoryCleanupTarget, WorkingDirectoryCurrentObservation,
|
||||||
pub enum MaterializerKind {
|
WorkingDirectoryOccupancy, WorkingDirectoryProvenance, WorkingDirectoryStatusKind,
|
||||||
#[default]
|
WorkingDirectorySummary,
|
||||||
LocalGitWorktree,
|
};
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
pub struct WorkingDirectoryRequest {
|
pub struct WorkingDirectoryRequest {
|
||||||
@@ -117,59 +116,6 @@ pub struct WorkingDirectoryClaim {
|
|||||||
pub relative_cwd: Option<String>,
|
pub relative_cwd: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
|
||||||
#[serde(rename_all = "snake_case")]
|
|
||||||
pub enum WorkingDirectoryStatusKind {
|
|
||||||
Active,
|
|
||||||
CleanupPending,
|
|
||||||
Corrupted,
|
|
||||||
NotFound,
|
|
||||||
Unknown,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
|
||||||
pub struct WorkingDirectoryCleanupTarget {
|
|
||||||
pub kind: String,
|
|
||||||
pub working_directory_id: String,
|
|
||||||
pub repository_id: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
|
||||||
pub struct WorkingDirectoryOccupancy {
|
|
||||||
#[serde(flatten)]
|
|
||||||
pub worker: RuntimeWorkerRef,
|
|
||||||
pub display_name: String,
|
|
||||||
pub linked_at: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
|
||||||
pub struct WorkingDirectorySummary {
|
|
||||||
pub working_directory_id: String,
|
|
||||||
pub repository_id: String,
|
|
||||||
/// Selector used to create this Workdir, retained as immutable materialization evidence.
|
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
||||||
pub creation_selector: Option<String>,
|
|
||||||
/// Provider-specific immutable ref resolved when this Workdir was created.
|
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
||||||
pub creation_ref: Option<String>,
|
|
||||||
/// Selector currently observed from the materialized Workdir, when one exists.
|
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
||||||
pub current_selector: Option<String>,
|
|
||||||
/// Provider-specific immutable ref currently observed from the materialized Workdir.
|
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
||||||
pub current_ref: Option<String>,
|
|
||||||
pub materializer_kind: MaterializerKind,
|
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
||||||
pub cleanup_target: Option<WorkingDirectoryCleanupTarget>,
|
|
||||||
pub status: WorkingDirectoryStatusKind,
|
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
||||||
pub cleanliness: Option<String>,
|
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
||||||
pub primary_worker_id: Option<WorkerId>,
|
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
||||||
pub occupied_by: Option<WorkingDirectoryOccupancy>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
pub struct WorkingDirectoryStatus {
|
pub struct WorkingDirectoryStatus {
|
||||||
pub summary: WorkingDirectorySummary,
|
pub summary: WorkingDirectorySummary,
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
|
pub use workdir::workspace::RuntimeWorkerRef;
|
||||||
|
|
||||||
/// Runtime-local Worker identity.
|
/// Runtime-local Worker identity.
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
|
||||||
@@ -30,29 +31,16 @@ impl fmt::Display for WorkerId {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Backend-visible Worker identity, namespaced by the Runtime that owns the Worker record.
|
/// Convert an opaque Workspace Worker reference only at the Runtime-local boundary.
|
||||||
///
|
impl TryFrom<&RuntimeWorkerRef> for WorkerRef {
|
||||||
/// This is intentionally distinct from [`WorkerRef`], which is meaningful only inside one
|
type Error = std::num::ParseIntError;
|
||||||
/// Runtime. Do not flatten this reference into a concatenated string for authority decisions.
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
|
|
||||||
pub struct RuntimeWorkerRef {
|
|
||||||
pub runtime_id: String,
|
|
||||||
pub worker_id: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RuntimeWorkerRef {
|
fn try_from(value: &RuntimeWorkerRef) -> Result<Self, Self::Error> {
|
||||||
pub fn new(runtime_id: impl Into<String>, worker_id: impl Into<String>) -> Self {
|
value
|
||||||
Self {
|
.worker_id
|
||||||
runtime_id: runtime_id.into(),
|
|
||||||
worker_id: worker_id.into(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn local_worker_ref(&self) -> Result<WorkerRef, std::num::ParseIntError> {
|
|
||||||
self.worker_id
|
|
||||||
.parse::<u64>()
|
.parse::<u64>()
|
||||||
.map(WorkerId::new)
|
.map(WorkerId::new)
|
||||||
.map(WorkerRef::new)
|
.map(Self::new)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,7 +66,7 @@ mod tests {
|
|||||||
assert_eq!(worker.runtime_id, "arcadia");
|
assert_eq!(worker.runtime_id, "arcadia");
|
||||||
assert_eq!(worker.worker_id, "30");
|
assert_eq!(worker.worker_id, "30");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
worker.local_worker_ref().unwrap(),
|
WorkerRef::try_from(&worker).unwrap(),
|
||||||
WorkerRef::new(WorkerId::new(30))
|
WorkerRef::new(WorkerId::new(30))
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@@ -90,6 +78,6 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn runtime_worker_ref_does_not_treat_composite_text_as_local_worker_id() {
|
fn runtime_worker_ref_does_not_treat_composite_text_as_local_worker_id() {
|
||||||
let worker = RuntimeWorkerRef::new("arcadia", "embedded-worker-runtime-5");
|
let worker = RuntimeWorkerRef::new("arcadia", "embedded-worker-runtime-5");
|
||||||
assert!(worker.local_worker_ref().is_err());
|
assert!(WorkerRef::try_from(&worker).is_err());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -467,8 +467,9 @@ impl Runtime {
|
|||||||
mut status: CatalogWorkingDirectoryStatus,
|
mut status: CatalogWorkingDirectoryStatus,
|
||||||
) -> Result<CatalogWorkingDirectoryStatus, RuntimeError> {
|
) -> Result<CatalogWorkingDirectoryStatus, RuntimeError> {
|
||||||
let state = self.lock()?;
|
let state = self.lock()?;
|
||||||
status.summary.primary_worker_id =
|
status.summary.primary_worker_id = state
|
||||||
state.primary_worker_id_for_workdir(status.summary.working_directory_id.as_str());
|
.primary_worker_id_for_workdir(status.summary.working_directory_id.as_str())
|
||||||
|
.map(|worker_id| worker_id.as_u64());
|
||||||
Ok(status)
|
Ok(status)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -154,7 +154,7 @@ impl WorkerObservationProvider for RuntimeGrantedWorkerObservationProvider {
|
|||||||
if grant.runtime_id != self.runtime_id {
|
if grant.runtime_id != self.runtime_id {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let Ok(worker_ref) = grant.local_worker_ref() else {
|
let Ok(worker_ref) = WorkerRef::try_from(grant) else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
let Some((workspace_id, state, _)) = self.hub.get(&worker_ref) else {
|
let Some((workspace_id, state, _)) = self.hub.get(&worker_ref) else {
|
||||||
@@ -192,9 +192,8 @@ impl WorkerObservationProvider for RuntimeGrantedWorkerObservationProvider {
|
|||||||
if !self.grants.contains(&grant) || runtime_id != &self.runtime_id {
|
if !self.grants.contains(&grant) || runtime_id != &self.runtime_id {
|
||||||
return Err(WorkerObservationError::NotFound);
|
return Err(WorkerObservationError::NotFound);
|
||||||
}
|
}
|
||||||
let worker_ref = grant
|
let worker_ref =
|
||||||
.local_worker_ref()
|
WorkerRef::try_from(&grant).map_err(|_| WorkerObservationError::NotFound)?;
|
||||||
.map_err(|_| WorkerObservationError::NotFound)?;
|
|
||||||
let (workspace_id, _, sink) = self
|
let (workspace_id, _, sink) = self
|
||||||
.hub
|
.hub
|
||||||
.get(&worker_ref)
|
.get(&worker_ref)
|
||||||
|
|||||||
@@ -14,6 +14,10 @@ use llm_engine::tool::{
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
use workdir::http::{WorkdirSessionOperation, WorkdirSessionOperationResult};
|
use workdir::http::{WorkdirSessionOperation, WorkdirSessionOperationResult};
|
||||||
|
use workdir::workspace::{
|
||||||
|
WorkingDirectoryDetailResponse as WorkdirDetailResponse,
|
||||||
|
WorkingDirectoryListResponse as WorkdirListResponse,
|
||||||
|
};
|
||||||
use workdir::{
|
use workdir::{
|
||||||
CommandHandle, CommandOutput, CommandOutputRequest, CommandRequest, CommandStatus, EditRequest,
|
CommandHandle, CommandOutput, CommandOutputRequest, CommandRequest, CommandStatus, EditRequest,
|
||||||
EditResult, GlobRequest, GlobResult, GrepRequest, GrepResult, ListRequest, ListResult,
|
EditResult, GlobRequest, GlobResult, GrepRequest, GrepResult, ListRequest, ListResult,
|
||||||
@@ -644,71 +648,6 @@ struct WorkdirDeleteInput {
|
|||||||
working_directory_id: String,
|
working_directory_id: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
|
|
||||||
struct WorkdirListResponse {
|
|
||||||
workspace_id: String,
|
|
||||||
items: Vec<WorkdirSummary>,
|
|
||||||
diagnostics: Vec<WorkdirDiagnostic>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
|
|
||||||
struct WorkdirDetailResponse {
|
|
||||||
workspace_id: String,
|
|
||||||
item: WorkdirSummary,
|
|
||||||
diagnostics: Vec<WorkdirDiagnostic>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
|
|
||||||
struct WorkdirSummary {
|
|
||||||
working_directory_id: String,
|
|
||||||
repository_id: String,
|
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
||||||
creation_selector: Option<String>,
|
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
||||||
creation_ref: Option<String>,
|
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
||||||
current_selector: Option<String>,
|
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
||||||
current_ref: Option<String>,
|
|
||||||
materializer_kind: String,
|
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
||||||
cleanup_target: Option<WorkdirCleanupTarget>,
|
|
||||||
status: String,
|
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
||||||
cleanliness: Option<String>,
|
|
||||||
#[serde(
|
|
||||||
default,
|
|
||||||
alias = "primary_worker_id",
|
|
||||||
skip_serializing_if = "Option::is_none"
|
|
||||||
)]
|
|
||||||
attached_worker_id: Option<u64>,
|
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
||||||
occupied_by: Option<WorkdirOccupancy>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
|
|
||||||
struct WorkdirCleanupTarget {
|
|
||||||
kind: String,
|
|
||||||
working_directory_id: String,
|
|
||||||
repository_id: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
|
|
||||||
struct WorkdirOccupancy {
|
|
||||||
runtime_id: String,
|
|
||||||
runtime_worker_id: u64,
|
|
||||||
worker_id: String,
|
|
||||||
display_name: String,
|
|
||||||
linked_at: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
|
|
||||||
struct WorkdirDiagnostic {
|
|
||||||
code: String,
|
|
||||||
severity: String,
|
|
||||||
message: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use std::sync::Mutex;
|
use std::sync::Mutex;
|
||||||
@@ -783,7 +722,13 @@ mod tests {
|
|||||||
"repository_id": "main"
|
"repository_id": "main"
|
||||||
},
|
},
|
||||||
"status": "active",
|
"status": "active",
|
||||||
"cleanliness": "clean"
|
"cleanliness": "clean",
|
||||||
|
"occupied_by": {
|
||||||
|
"runtime_id": "arcadia",
|
||||||
|
"worker_id": "worker-opaque-64",
|
||||||
|
"display_name": "Coder",
|
||||||
|
"linked_at": "2026-08-12T00:00:00Z"
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -889,6 +834,18 @@ mod tests {
|
|||||||
assert_eq!(delete_schema()["required"], json!(["working_directory_id"]));
|
assert_eq!(delete_schema()["required"], json!(["working_directory_id"]));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn legacy_runtime_worker_id_is_rejected_by_workspace_workdir_contract() {
|
||||||
|
let mut response = json!({
|
||||||
|
"workspace_id": "workspace/one",
|
||||||
|
"items": [workdir_json("wd-1")],
|
||||||
|
"diagnostics": []
|
||||||
|
});
|
||||||
|
response["items"][0]["occupied_by"]["runtime_worker_id"] = json!(64);
|
||||||
|
|
||||||
|
assert!(serde_json::from_value::<WorkdirListResponse>(response).is_err());
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn list_create_explicit_attach_detach_and_delete_use_scoped_workspace_authority_paths() {
|
fn list_create_explicit_attach_detach_and_delete_use_scoped_workspace_authority_paths() {
|
||||||
let client = Arc::new(RecordingWorkspaceClient::new(vec![
|
let client = Arc::new(RecordingWorkspaceClient::new(vec![
|
||||||
@@ -925,7 +882,18 @@ mod tests {
|
|||||||
]));
|
]));
|
||||||
let backend = WorkspaceHttpWorkdirBackend::new(client.clone());
|
let backend = WorkspaceHttpWorkdirBackend::new(client.clone());
|
||||||
|
|
||||||
backend.list().unwrap();
|
let listed = backend.list().unwrap();
|
||||||
|
let listed: serde_json::Value =
|
||||||
|
serde_json::from_str(listed.content.as_deref().unwrap()).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
listed["items"][0]["occupied_by"]["worker_id"],
|
||||||
|
"worker-opaque-64"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
listed["items"][0]["occupied_by"]
|
||||||
|
.get("runtime_worker_id")
|
||||||
|
.is_none()
|
||||||
|
);
|
||||||
let created = backend
|
let created = backend
|
||||||
.create(WorkdirCreateInput {
|
.create(WorkdirCreateInput {
|
||||||
runtime_id: "runtime/one".to_string(),
|
runtime_id: "runtime/one".to_string(),
|
||||||
|
|||||||
@@ -44,6 +44,13 @@ use webauthn_rs::prelude::{
|
|||||||
};
|
};
|
||||||
use workdir::WorkdirSessionHandle;
|
use workdir::WorkdirSessionHandle;
|
||||||
use workdir::http::{WorkdirSessionOperation, WorkdirSessionOperationResult};
|
use workdir::http::{WorkdirSessionOperation, WorkdirSessionOperationResult};
|
||||||
|
use workdir::workspace::{
|
||||||
|
MaterializerKind, WorkingDirectoryCleanupTarget,
|
||||||
|
WorkingDirectoryDetailResponse as BrowserWorkingDirectoryDetailResponse,
|
||||||
|
WorkingDirectoryDiagnostic, WorkingDirectoryDiagnosticSeverity,
|
||||||
|
WorkingDirectoryListResponse as BrowserWorkingDirectoryListResponse, WorkingDirectoryOccupancy,
|
||||||
|
WorkingDirectoryStatusKind, WorkingDirectorySummary,
|
||||||
|
};
|
||||||
use worker::feature::builtin::{WorkerObservationSubject, WorkerObservationSubjectRef};
|
use worker::feature::builtin::{WorkerObservationSubject, WorkerObservationSubjectRef};
|
||||||
use worker_runtime::resource::{BackendResourceError, BackendResourceFetchRequest};
|
use worker_runtime::resource::{BackendResourceError, BackendResourceFetchRequest};
|
||||||
use worker_runtime::worker_backend::{ProfileRuntimeWorkerFactory, WorkerRuntimeExecutionBackend};
|
use worker_runtime::worker_backend::{ProfileRuntimeWorkerFactory, WorkerRuntimeExecutionBackend};
|
||||||
@@ -101,10 +108,8 @@ use crate::store::{
|
|||||||
};
|
};
|
||||||
use crate::{Error, Result};
|
use crate::{Error, Result};
|
||||||
use worker_runtime::catalog::{
|
use worker_runtime::catalog::{
|
||||||
ConfigBundleRef, MaterializerKind, ProfileSelector,
|
ConfigBundleRef, ProfileSelector, RepositorySelector as RuntimeRepositorySelector,
|
||||||
RepositorySelector as RuntimeRepositorySelector, WorkingDirectoryClaim,
|
WorkingDirectoryClaim, WorkingDirectoryRepository, WorkingDirectoryRequest, WorkspaceApiRef,
|
||||||
WorkingDirectoryOccupancy, WorkingDirectoryRepository, WorkingDirectoryRequest,
|
|
||||||
WorkingDirectoryStatusKind, WorkingDirectorySummary, WorkspaceApiRef,
|
|
||||||
};
|
};
|
||||||
use worker_runtime::config_bundle::ConfigBundle;
|
use worker_runtime::config_bundle::ConfigBundle;
|
||||||
use worker_runtime::http_server::{
|
use worker_runtime::http_server::{
|
||||||
@@ -2027,20 +2032,6 @@ pub struct BrowserWorkingDirectoryCreateRequest {
|
|||||||
pub selector: Option<String>,
|
pub selector: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
|
||||||
pub struct BrowserWorkingDirectoryListResponse {
|
|
||||||
pub workspace_id: String,
|
|
||||||
pub items: Vec<WorkingDirectorySummary>,
|
|
||||||
pub diagnostics: Vec<RuntimeDiagnostic>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
|
||||||
pub struct BrowserWorkingDirectoryDetailResponse {
|
|
||||||
pub workspace_id: String,
|
|
||||||
pub item: WorkingDirectorySummary,
|
|
||||||
pub diagnostics: Vec<RuntimeDiagnostic>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
#[serde(deny_unknown_fields)]
|
#[serde(deny_unknown_fields)]
|
||||||
pub struct BrowserWorkerWorkingDirectorySelection {
|
pub struct BrowserWorkerWorkingDirectorySelection {
|
||||||
@@ -5834,6 +5825,23 @@ async fn scoped_get_worker_launch_options(
|
|||||||
get_worker_launch_options(State(api)).await
|
get_worker_launch_options(State(api)).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn working_directory_diagnostics(
|
||||||
|
diagnostics: Vec<RuntimeDiagnostic>,
|
||||||
|
) -> Vec<WorkingDirectoryDiagnostic> {
|
||||||
|
diagnostics
|
||||||
|
.into_iter()
|
||||||
|
.map(|diagnostic| WorkingDirectoryDiagnostic {
|
||||||
|
code: diagnostic.code,
|
||||||
|
severity: match diagnostic.severity {
|
||||||
|
DiagnosticSeverity::Info => WorkingDirectoryDiagnosticSeverity::Info,
|
||||||
|
DiagnosticSeverity::Warning => WorkingDirectoryDiagnosticSeverity::Warning,
|
||||||
|
DiagnosticSeverity::Error => WorkingDirectoryDiagnosticSeverity::Error,
|
||||||
|
},
|
||||||
|
message: diagnostic.message,
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
async fn scoped_list_runtime_working_directories(
|
async fn scoped_list_runtime_working_directories(
|
||||||
State(api): State<WorkspaceApi>,
|
State(api): State<WorkspaceApi>,
|
||||||
AxumPath(path): AxumPath<ScopedRuntimePath>,
|
AxumPath(path): AxumPath<ScopedRuntimePath>,
|
||||||
@@ -5843,7 +5851,7 @@ async fn scoped_list_runtime_working_directories(
|
|||||||
Ok(Json(BrowserWorkingDirectoryListResponse {
|
Ok(Json(BrowserWorkingDirectoryListResponse {
|
||||||
workspace_id: api.config.workspace_id.clone(),
|
workspace_id: api.config.workspace_id.clone(),
|
||||||
items,
|
items,
|
||||||
diagnostics,
|
diagnostics: working_directory_diagnostics(diagnostics),
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -5995,7 +6003,7 @@ fn create_working_directory_for_runtime(
|
|||||||
Ok(Json(BrowserWorkingDirectoryDetailResponse {
|
Ok(Json(BrowserWorkingDirectoryDetailResponse {
|
||||||
workspace_id: api.config.workspace_id.clone(),
|
workspace_id: api.config.workspace_id.clone(),
|
||||||
item: summary,
|
item: summary,
|
||||||
diagnostics: result.diagnostics,
|
diagnostics: working_directory_diagnostics(result.diagnostics),
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -6016,7 +6024,7 @@ fn working_directory_detail_for_runtime(
|
|||||||
return Ok(Json(BrowserWorkingDirectoryDetailResponse {
|
return Ok(Json(BrowserWorkingDirectoryDetailResponse {
|
||||||
workspace_id: api.config.workspace_id.clone(),
|
workspace_id: api.config.workspace_id.clone(),
|
||||||
item: summary,
|
item: summary,
|
||||||
diagnostics: result.diagnostics,
|
diagnostics: working_directory_diagnostics(result.diagnostics),
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
if let Some(record) = api
|
if let Some(record) = api
|
||||||
@@ -6026,7 +6034,7 @@ fn working_directory_detail_for_runtime(
|
|||||||
return Ok(Json(BrowserWorkingDirectoryDetailResponse {
|
return Ok(Json(BrowserWorkingDirectoryDetailResponse {
|
||||||
workspace_id: api.config.workspace_id.clone(),
|
workspace_id: api.config.workspace_id.clone(),
|
||||||
item: projected_workdir_summary_from_record(&api, &record)?,
|
item: projected_workdir_summary_from_record(&api, &record)?,
|
||||||
diagnostics: result.diagnostics,
|
diagnostics: working_directory_diagnostics(result.diagnostics),
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
Err(ApiError::with_diagnostics(
|
Err(ApiError::with_diagnostics(
|
||||||
@@ -6085,7 +6093,7 @@ fn cleanup_working_directory_for_runtime(
|
|||||||
Ok(Json(BrowserWorkingDirectoryDetailResponse {
|
Ok(Json(BrowserWorkingDirectoryDetailResponse {
|
||||||
workspace_id: api.config.workspace_id.clone(),
|
workspace_id: api.config.workspace_id.clone(),
|
||||||
item: summary,
|
item: summary,
|
||||||
diagnostics: result.diagnostics,
|
diagnostics: working_directory_diagnostics(result.diagnostics),
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -10834,7 +10842,7 @@ fn workdir_summary_from_record(record: &WorkdirRegistryRecord) -> WorkingDirecto
|
|||||||
current_selector: record.current_selector.clone(),
|
current_selector: record.current_selector.clone(),
|
||||||
current_ref: record.current_ref.clone(),
|
current_ref: record.current_ref.clone(),
|
||||||
materializer_kind: MaterializerKind::LocalGitWorktree,
|
materializer_kind: MaterializerKind::LocalGitWorktree,
|
||||||
cleanup_target: Some(worker_runtime::catalog::WorkingDirectoryCleanupTarget {
|
cleanup_target: Some(WorkingDirectoryCleanupTarget {
|
||||||
kind: "local_git_worktree".to_string(),
|
kind: "local_git_worktree".to_string(),
|
||||||
working_directory_id: record.workdir_id.clone(),
|
working_directory_id: record.workdir_id.clone(),
|
||||||
repository_id: record.repository_id.clone(),
|
repository_id: record.repository_id.clone(),
|
||||||
@@ -17569,6 +17577,43 @@ mod tests {
|
|||||||
assert_eq!(mutation.status(), StatusCode::METHOD_NOT_ALLOWED);
|
assert_eq!(mutation.status(), StatusCode::METHOD_NOT_ALLOWED);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn workspace_workdir_response_serializes_shared_occupied_contract() {
|
||||||
|
let response = BrowserWorkingDirectoryListResponse {
|
||||||
|
workspace_id: TEST_WORKSPACE_ID.to_string(),
|
||||||
|
items: vec![WorkingDirectorySummary {
|
||||||
|
working_directory_id: "wd-1".to_string(),
|
||||||
|
repository_id: "main".to_string(),
|
||||||
|
creation_selector: None,
|
||||||
|
creation_ref: None,
|
||||||
|
current_selector: Some("work/ticket".to_string()),
|
||||||
|
current_ref: Some("abc123".to_string()),
|
||||||
|
materializer_kind: MaterializerKind::LocalGitWorktree,
|
||||||
|
cleanup_target: None,
|
||||||
|
status: WorkingDirectoryStatusKind::Active,
|
||||||
|
cleanliness: Some("clean".to_string()),
|
||||||
|
primary_worker_id: None,
|
||||||
|
occupied_by: Some(WorkingDirectoryOccupancy {
|
||||||
|
worker: RuntimeWorkerRef::new("arcadia", "worker-opaque-64"),
|
||||||
|
display_name: "Coder".to_string(),
|
||||||
|
linked_at: "2026-08-12T00:00:00Z".to_string(),
|
||||||
|
}),
|
||||||
|
}],
|
||||||
|
diagnostics: vec![],
|
||||||
|
};
|
||||||
|
|
||||||
|
let value = serde_json::to_value(response).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
value["items"][0]["occupied_by"]["worker_id"],
|
||||||
|
"worker-opaque-64"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
value["items"][0]["occupied_by"]
|
||||||
|
.get("runtime_worker_id")
|
||||||
|
.is_none()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
async fn get_json(app: Router, uri: &str) -> Value {
|
async fn get_json(app: Router, uri: &str) -> Value {
|
||||||
let response = app
|
let response = app
|
||||||
.oneshot(Request::builder().uri(uri).body(Body::empty()).unwrap())
|
.oneshot(Request::builder().uri(uri).body(Body::empty()).unwrap())
|
||||||
|
|||||||
Reference in New Issue
Block a user