refactor: centralize workdir REST DTOs
This commit is contained in:
Generated
-2
@@ -649,7 +649,6 @@ dependencies = [
|
|||||||
"tokio",
|
"tokio",
|
||||||
"tokio-tungstenite 0.29.0",
|
"tokio-tungstenite 0.29.0",
|
||||||
"uuid",
|
"uuid",
|
||||||
"workdir",
|
|
||||||
"workspace-api",
|
"workspace-api",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -6684,7 +6683,6 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"ts-rs",
|
"ts-rs",
|
||||||
"workdir",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ tokio = { workspace = true, features = ["rt", "macros", "net", "io-util", "sync"
|
|||||||
tokio-tungstenite = { workspace = true }
|
tokio-tungstenite = { workspace = true }
|
||||||
uuid = { workspace = true }
|
uuid = { workspace = true }
|
||||||
workspace-api.workspace = true
|
workspace-api.workspace = true
|
||||||
workdir = { workspace = true }
|
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
tempfile = { workspace = true }
|
tempfile = { workspace = true }
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ use tokio_tungstenite::tungstenite::Message as TungsteniteMessage;
|
|||||||
use tokio_tungstenite::tungstenite::client::IntoClientRequest;
|
use tokio_tungstenite::tungstenite::client::IntoClientRequest;
|
||||||
use tokio_tungstenite::tungstenite::http::HeaderValue;
|
use tokio_tungstenite::tungstenite::http::HeaderValue;
|
||||||
use tokio_tungstenite::tungstenite::http::header::AUTHORIZATION;
|
use tokio_tungstenite::tungstenite::http::header::AUTHORIZATION;
|
||||||
pub use workdir::workspace::WorkingDirectorySummary as BackendWorkingDirectorySummary;
|
|
||||||
pub use workspace_api::{
|
pub use workspace_api::{
|
||||||
Diagnostic as BackendDiagnostic, DiagnosticSeverity as BackendDiagnosticSeverity,
|
Diagnostic as BackendDiagnostic, DiagnosticSeverity as BackendDiagnosticSeverity,
|
||||||
ListResponse as BackendRuntimeListResponse, RuntimeSummary as BackendRuntimeSummary,
|
ListResponse as BackendRuntimeListResponse, RuntimeSummary as BackendRuntimeSummary,
|
||||||
@@ -20,6 +19,11 @@ pub use workspace_api::{
|
|||||||
WorkerRestoreResponse as BackendWorkerRestoreResponse,
|
WorkerRestoreResponse as BackendWorkerRestoreResponse,
|
||||||
WorkerRestoreResult as BackendWorkerRestoreResult, WorkerSummary as BackendWorkerSummary,
|
WorkerRestoreResult as BackendWorkerRestoreResult, WorkerSummary as BackendWorkerSummary,
|
||||||
WorkerWorkspaceSummary as BackendWorkerWorkspaceSummary,
|
WorkerWorkspaceSummary as BackendWorkerWorkspaceSummary,
|
||||||
|
WorkingDirectoryCreateRequest as BackendWorkingDirectoryCreateRequest,
|
||||||
|
WorkingDirectoryCreateResponse as BackendWorkingDirectoryCreateResponse,
|
||||||
|
WorkingDirectoryDetailResponse as BackendWorkingDirectoryDetailResponse,
|
||||||
|
WorkingDirectoryListResponse as BackendWorkingDirectoryListResponse,
|
||||||
|
WorkingDirectorySummary as BackendWorkingDirectorySummary,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
@@ -675,8 +679,8 @@ mod tests {
|
|||||||
.unwrap()
|
.unwrap()
|
||||||
.occupied_by
|
.occupied_by
|
||||||
.expect("occupied Workdir");
|
.expect("occupied Workdir");
|
||||||
assert_eq!(occupied_by.worker.runtime_id, "arcadia");
|
assert_eq!(occupied_by.runtime_id, "arcadia");
|
||||||
assert_eq!(occupied_by.worker.worker_id, "worker-opaque-64");
|
assert_eq!(occupied_by.worker_id, "worker-opaque-64");
|
||||||
|
|
||||||
let mut stale = payload;
|
let mut stale = payload;
|
||||||
stale["working_directory"]["occupied_by"]["runtime_worker_id"] = serde_json::json!(64);
|
stale["working_directory"]["occupied_by"]["runtime_worker_id"] = serde_json::json!(64);
|
||||||
|
|||||||
@@ -200,39 +200,6 @@ impl WorkingDirectorySummary {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[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 runtime_id: String,
|
|
||||||
pub item: WorkingDirectorySummary,
|
|
||||||
pub diagnostics: Vec<WorkingDirectoryDiagnostic>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
@@ -253,91 +220,6 @@ mod tests {
|
|||||||
assert_eq!(serde_json::to_value(status).unwrap(), expected);
|
assert_eq!(serde_json::to_value(status).unwrap(), expected);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[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()),
|
|
||||||
creation_tree: Some("tree123".to_string()),
|
|
||||||
current_selector: Some("work/ticket".to_string()),
|
|
||||||
current_ref: Some("def456".to_string()),
|
|
||||||
current_tree: Some("tree456".to_string()),
|
|
||||||
observed_at_epoch_seconds: Some(1_777_777_777),
|
|
||||||
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,
|
|
||||||
creation_tree: None,
|
|
||||||
current_selector: None,
|
|
||||||
current_ref: Some("987fed".to_string()),
|
|
||||||
current_tree: None,
|
|
||||||
observed_at_epoch_seconds: None,
|
|
||||||
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(),
|
|
||||||
runtime_id: "arcadia".to_string(),
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
|||||||
@@ -12,11 +12,7 @@ use async_trait::async_trait;
|
|||||||
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::{
|
use workdir::workspace::{WorkspaceWorkdirSessionFence, WorkspaceWorkdirSessionOperationRequest};
|
||||||
WorkingDirectoryDetailResponse as WorkdirDetailResponse,
|
|
||||||
WorkingDirectoryListResponse as WorkdirListResponse, WorkspaceWorkdirSessionFence,
|
|
||||||
WorkspaceWorkdirSessionOperationRequest,
|
|
||||||
};
|
|
||||||
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,
|
||||||
@@ -24,6 +20,11 @@ use workdir::{
|
|||||||
WorkdirSessionCapabilities, WorkdirSessionHandle, WriteRequest, WriteResult,
|
WorkdirSessionCapabilities, WorkdirSessionHandle, WriteRequest, WriteResult,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use workspace_api::{
|
||||||
|
WorkingDirectoryDetailResponse as WorkdirDetailResponse,
|
||||||
|
WorkingDirectoryListResponse as WorkdirListResponse,
|
||||||
|
};
|
||||||
|
|
||||||
use crate::feature::{
|
use crate::feature::{
|
||||||
FeatureDescriptor, FeatureInstallContext, FeatureInstallError, FeatureModule, ToolContribution,
|
FeatureDescriptor, FeatureInstallContext, FeatureInstallError, FeatureModule, ToolContribution,
|
||||||
ToolDeclaration,
|
ToolDeclaration,
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ typescript = ["dep:ts-rs"]
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
serde = { workspace = true, features = ["derive"] }
|
serde = { workspace = true, features = ["derive"] }
|
||||||
ts-rs = { version = "12.0.1", optional = true }
|
ts-rs = { version = "12.0.1", optional = true }
|
||||||
workdir.workspace = true
|
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
serde_json.workspace = true
|
serde_json.workspace = true
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
fn main() {
|
||||||
|
print!("{}", workspace_api::workdir_api_typescript());
|
||||||
|
}
|
||||||
@@ -5,7 +5,6 @@
|
|||||||
//! callers must explicitly construct these Workspace-authoritative resources.
|
//! callers must explicitly construct these Workspace-authoritative resources.
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use workdir::workspace::WorkingDirectorySummary;
|
|
||||||
|
|
||||||
/// Provider-neutral classification of an authoritative Repository source.
|
/// Provider-neutral classification of an authoritative Repository source.
|
||||||
///
|
///
|
||||||
@@ -93,6 +92,7 @@ pub const TICKET_RELATIONS_QUERY_PATH: &str = "/tickets/relations/search";
|
|||||||
pub const TICKET_ORCHESTRATION_PLANS_QUERY_PATH: &str = "/tickets/orchestration-plans/search";
|
pub const TICKET_ORCHESTRATION_PLANS_QUERY_PATH: &str = "/tickets/orchestration-plans/search";
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||||
#[serde(rename_all = "snake_case")]
|
#[serde(rename_all = "snake_case")]
|
||||||
pub enum DiagnosticSeverity {
|
pub enum DiagnosticSeverity {
|
||||||
Info,
|
Info,
|
||||||
@@ -101,12 +101,140 @@ pub enum DiagnosticSeverity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||||
|
#[serde(deny_unknown_fields)]
|
||||||
pub struct Diagnostic {
|
pub struct Diagnostic {
|
||||||
pub code: String,
|
pub code: String,
|
||||||
pub severity: DiagnosticSeverity,
|
pub severity: DiagnosticSeverity,
|
||||||
pub message: String,
|
pub message: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Public Workdir materializer classification.
|
||||||
|
///
|
||||||
|
/// The value identifies stable materialization provenance without exposing a
|
||||||
|
/// provider path, Runtime handle, or session identity.
|
||||||
|
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
pub enum WorkingDirectoryMaterializerKind {
|
||||||
|
RuntimeGitCache,
|
||||||
|
LocalGitWorktree,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
pub enum WorkingDirectoryStatusKind {
|
||||||
|
Active,
|
||||||
|
CleanupPending,
|
||||||
|
Corrupted,
|
||||||
|
NotFound,
|
||||||
|
Unknown,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||||
|
#[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(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||||
|
#[serde(deny_unknown_fields)]
|
||||||
|
pub struct WorkingDirectoryOccupancy {
|
||||||
|
pub runtime_id: String,
|
||||||
|
pub worker_id: String,
|
||||||
|
pub display_name: String,
|
||||||
|
pub linked_at: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Public, provider-neutral Workdir inventory projection.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||||
|
#[cfg_attr(feature = "typescript", ts(optional_fields = nullable))]
|
||||||
|
#[serde(deny_unknown_fields)]
|
||||||
|
pub struct WorkingDirectorySummary {
|
||||||
|
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 creation_tree: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub current_selector: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub current_ref: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub current_tree: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
#[cfg_attr(feature = "typescript", ts(optional, type = "number | null"))]
|
||||||
|
pub observed_at_epoch_seconds: Option<u64>,
|
||||||
|
pub materializer_kind: WorkingDirectoryMaterializerKind,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub cleanup_target: Option<WorkingDirectoryCleanupTarget>,
|
||||||
|
pub status: WorkingDirectoryStatusKind,
|
||||||
|
#[serde(default)]
|
||||||
|
pub cleanliness: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub primary_worker_id: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub occupied_by: Option<WorkingDirectoryOccupancy>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Browser/Rust-client Workdir materialization request.
|
||||||
|
///
|
||||||
|
/// `runtime_id = None` requests Workspace default Runtime resolution and
|
||||||
|
/// `operation_id = Some(_)` fences exact replay. All four fields deliberately
|
||||||
|
/// preserve the Server's existing optionality.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||||
|
#[cfg_attr(feature = "typescript", ts(optional_fields = nullable))]
|
||||||
|
#[serde(deny_unknown_fields)]
|
||||||
|
pub struct WorkingDirectoryCreateRequest {
|
||||||
|
#[serde(default)]
|
||||||
|
pub runtime_id: Option<String>,
|
||||||
|
pub repository_id: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub selector: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub operation_id: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||||
|
#[serde(deny_unknown_fields)]
|
||||||
|
pub struct WorkingDirectoryListResponse {
|
||||||
|
pub workspace_id: String,
|
||||||
|
pub items: Vec<WorkingDirectorySummary>,
|
||||||
|
pub diagnostics: Vec<Diagnostic>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||||
|
#[serde(deny_unknown_fields)]
|
||||||
|
pub struct WorkingDirectoryDetailResponse {
|
||||||
|
pub workspace_id: String,
|
||||||
|
pub runtime_id: String,
|
||||||
|
pub item: WorkingDirectorySummary,
|
||||||
|
pub diagnostics: Vec<Diagnostic>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||||
|
#[serde(deny_unknown_fields)]
|
||||||
|
pub struct WorkingDirectoryCreateResponse {
|
||||||
|
pub workspace_id: String,
|
||||||
|
pub runtime_id: String,
|
||||||
|
pub item: WorkingDirectorySummary,
|
||||||
|
pub diagnostics: Vec<Diagnostic>,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
pub struct ListResponse<T> {
|
pub struct ListResponse<T> {
|
||||||
pub workspace_id: String,
|
pub workspace_id: String,
|
||||||
@@ -537,6 +665,61 @@ pub struct RepositoryAccessProjection {
|
|||||||
pub bindings: Vec<RepositorySshAccessBinding>,
|
pub bindings: Vec<RepositorySshAccessBinding>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "typescript")]
|
||||||
|
pub fn workdir_api_typescript() -> String {
|
||||||
|
use ts_rs::TS;
|
||||||
|
|
||||||
|
let config = ts_rs::Config::default();
|
||||||
|
let declarations = [
|
||||||
|
DiagnosticSeverity::decl(&config),
|
||||||
|
Diagnostic::decl(&config),
|
||||||
|
WorkingDirectoryMaterializerKind::decl(&config),
|
||||||
|
WorkingDirectoryStatusKind::decl(&config),
|
||||||
|
WorkingDirectoryCleanupTarget::decl(&config),
|
||||||
|
WorkingDirectoryOccupancy::decl(&config),
|
||||||
|
WorkingDirectorySummary::decl(&config),
|
||||||
|
WorkingDirectoryCreateRequest::decl(&config),
|
||||||
|
WorkingDirectoryListResponse::decl(&config),
|
||||||
|
WorkingDirectoryDetailResponse::decl(&config),
|
||||||
|
WorkingDirectoryCreateResponse::decl(&config),
|
||||||
|
];
|
||||||
|
format!(
|
||||||
|
"// Generated from workspace-api. Do not edit by hand.\n// Regenerate: cargo run -q -p workspace-api --features typescript --example generate_workdir_api_types > web/workspace/src/lib/generated/workdir-api.ts\n\n{}\n",
|
||||||
|
declarations
|
||||||
|
.into_iter()
|
||||||
|
.map(|declaration| format!("export {declaration}"))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join("\n\n")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(all(test, feature = "typescript"))]
|
||||||
|
mod typescript_tests {
|
||||||
|
#[test]
|
||||||
|
fn generated_workdir_api_contract_is_current() {
|
||||||
|
let expected = super::workdir_api_typescript();
|
||||||
|
let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||||
|
.join("../../web/workspace/src/lib/generated/workdir-api.ts");
|
||||||
|
let actual = std::fs::read_to_string(&path)
|
||||||
|
.unwrap_or_else(|error| panic!("failed to read {}: {error}", path.display()));
|
||||||
|
assert_eq!(
|
||||||
|
normalize(&actual),
|
||||||
|
normalize(&expected),
|
||||||
|
"regenerate Workdir API TypeScript types with `cargo run -q -p workspace-api --features typescript --example generate_workdir_api_types > web/workspace/src/lib/generated/workdir-api.ts` and format the generated file",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn normalize(value: &str) -> String {
|
||||||
|
value
|
||||||
|
.chars()
|
||||||
|
.filter_map(|character| match character {
|
||||||
|
'\r' | '\n' | ' ' | '\t' => None,
|
||||||
|
_ => Some(character),
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
@@ -560,4 +743,52 @@ mod tests {
|
|||||||
|
|
||||||
assert!(serde_json::from_value::<WorkerSummary>(payload).is_err());
|
assert!(serde_json::from_value::<WorkerSummary>(payload).is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn workdir_create_request_preserves_optional_operation_fields() {
|
||||||
|
let payload = serde_json::json!({"repository_id": "main"});
|
||||||
|
let request = serde_json::from_value::<WorkingDirectoryCreateRequest>(payload)
|
||||||
|
.expect("optional create fields may be absent");
|
||||||
|
|
||||||
|
assert_eq!(request.runtime_id, None);
|
||||||
|
assert_eq!(request.selector, None);
|
||||||
|
assert_eq!(request.operation_id, None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn workdir_create_request_rejects_stale_or_incomplete_json() {
|
||||||
|
let stale = serde_json::json!({
|
||||||
|
"repository_id": "main",
|
||||||
|
"selector": "develop",
|
||||||
|
"path": "/tmp/workdir"
|
||||||
|
});
|
||||||
|
assert!(serde_json::from_value::<WorkingDirectoryCreateRequest>(stale).is_err());
|
||||||
|
|
||||||
|
let incomplete = serde_json::json!({
|
||||||
|
"runtime_id": "arcadia",
|
||||||
|
"operation_id": "operation-1"
|
||||||
|
});
|
||||||
|
assert!(serde_json::from_value::<WorkingDirectoryCreateRequest>(incomplete).is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn workdir_response_rejects_stale_occupancy_shape() {
|
||||||
|
let stale = serde_json::json!({
|
||||||
|
"workspace_id": "workspace-test",
|
||||||
|
"items": [{
|
||||||
|
"working_directory_id": "workdir-1",
|
||||||
|
"repository_id": "main",
|
||||||
|
"materializer_kind": "runtime_git_cache",
|
||||||
|
"status": "active",
|
||||||
|
"occupied_by": {
|
||||||
|
"runtime_worker_id": "worker-1",
|
||||||
|
"display_name": "Coder",
|
||||||
|
"linked_at": "2026-01-01T00:00:00Z"
|
||||||
|
}
|
||||||
|
}],
|
||||||
|
"diagnostics": []
|
||||||
|
});
|
||||||
|
|
||||||
|
assert!(serde_json::from_value::<WorkingDirectoryListResponse>(stale).is_err());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -341,7 +341,7 @@ pub(crate) fn workspace_worker_summary(
|
|||||||
can_stop: summary.capabilities.can_stop,
|
can_stop: summary.capabilities.can_stop,
|
||||||
can_spawn_followup: summary.capabilities.can_spawn_followup,
|
can_spawn_followup: summary.capabilities.can_spawn_followup,
|
||||||
},
|
},
|
||||||
working_directory: summary.working_directory,
|
working_directory: summary.working_directory.map(crate::workdir_api::summary),
|
||||||
diagnostics: summary.diagnostics.into_iter().map(Into::into).collect(),
|
diagnostics: summary.diagnostics.into_iter().map(Into::into).collect(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ pub mod runtime_subscription;
|
|||||||
pub mod server;
|
pub mod server;
|
||||||
pub mod skills;
|
pub mod skills;
|
||||||
pub mod store;
|
pub mod store;
|
||||||
|
mod workdir_api;
|
||||||
pub mod workdir_create_operations;
|
pub mod workdir_create_operations;
|
||||||
pub mod worker_source;
|
pub mod worker_source;
|
||||||
pub mod workspace_catalog;
|
pub mod workspace_catalog;
|
||||||
|
|||||||
@@ -48,10 +48,7 @@ use workdir::http::{
|
|||||||
WorkdirSessionOperation, WorkdirSessionOperationResult, WorkdirTransportError,
|
WorkdirSessionOperation, WorkdirSessionOperationResult, WorkdirTransportError,
|
||||||
};
|
};
|
||||||
use workdir::workspace::{
|
use workdir::workspace::{
|
||||||
MaterializerKind, WorkingDirectoryCleanupTarget,
|
MaterializerKind, WorkingDirectoryCleanupTarget, WorkingDirectoryOccupancy,
|
||||||
WorkingDirectoryDetailResponse as BrowserWorkingDirectoryDetailResponse,
|
|
||||||
WorkingDirectoryDiagnostic, WorkingDirectoryDiagnosticSeverity,
|
|
||||||
WorkingDirectoryListResponse as BrowserWorkingDirectoryListResponse, WorkingDirectoryOccupancy,
|
|
||||||
WorkingDirectoryStatusKind, WorkingDirectorySummary, WorkspaceWorkdirSessionFence,
|
WorkingDirectoryStatusKind, WorkingDirectorySummary, WorkspaceWorkdirSessionFence,
|
||||||
WorkspaceWorkdirSessionOperationRequest,
|
WorkspaceWorkdirSessionOperationRequest,
|
||||||
};
|
};
|
||||||
@@ -66,8 +63,12 @@ use workspace_api::{
|
|||||||
ObjectiveStateRequest, PutRepositorySshHostTrustRequest, RepositoryAccessProjection,
|
ObjectiveStateRequest, PutRepositorySshHostTrustRequest, RepositoryAccessProjection,
|
||||||
RepositorySshCredential, RepositorySshHostTrust, RotateRepositorySshCredentialRequest,
|
RepositorySshCredential, RepositorySshHostTrust, RotateRepositorySshCredentialRequest,
|
||||||
RuntimeConnectionTestResponse, RuntimeManagementSummary, TICKET_ORCHESTRATION_PLANS_QUERY_PATH,
|
RuntimeConnectionTestResponse, RuntimeManagementSummary, TICKET_ORCHESTRATION_PLANS_QUERY_PATH,
|
||||||
TICKET_RELATIONS_QUERY_PATH, WorkspaceRuntimeResource, WorkspaceWorkerDiscoveryItem,
|
TICKET_RELATIONS_QUERY_PATH,
|
||||||
WorkspaceWorkerDiscoveryPage, WorkspaceWorkerSubject,
|
WorkingDirectoryCreateRequest as BrowserWorkingDirectoryCreateRequest,
|
||||||
|
WorkingDirectoryCreateResponse as BrowserWorkingDirectoryCreateResponse,
|
||||||
|
WorkingDirectoryDetailResponse as BrowserWorkingDirectoryDetailResponse,
|
||||||
|
WorkingDirectoryListResponse as BrowserWorkingDirectoryListResponse, WorkspaceRuntimeResource,
|
||||||
|
WorkspaceWorkerDiscoveryItem, WorkspaceWorkerDiscoveryPage, WorkspaceWorkerSubject,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::auth::{
|
use crate::auth::{
|
||||||
@@ -2994,18 +2995,6 @@ pub struct WorkingDirectoryRepositoryOption {
|
|||||||
pub default_selector: Option<String>,
|
pub default_selector: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize)]
|
|
||||||
#[serde(deny_unknown_fields)]
|
|
||||||
pub struct BrowserWorkingDirectoryCreateRequest {
|
|
||||||
#[serde(default)]
|
|
||||||
pub runtime_id: Option<String>,
|
|
||||||
pub repository_id: String,
|
|
||||||
#[serde(default)]
|
|
||||||
pub selector: Option<String>,
|
|
||||||
#[serde(default)]
|
|
||||||
pub operation_id: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
#[serde(deny_unknown_fields)]
|
#[serde(deny_unknown_fields)]
|
||||||
pub struct BrowserWorkerWorkingDirectorySelection {
|
pub struct BrowserWorkerWorkingDirectorySelection {
|
||||||
@@ -8827,15 +8816,15 @@ async fn scoped_get_worker_launch_options(
|
|||||||
|
|
||||||
fn working_directory_diagnostics(
|
fn working_directory_diagnostics(
|
||||||
diagnostics: Vec<RuntimeDiagnostic>,
|
diagnostics: Vec<RuntimeDiagnostic>,
|
||||||
) -> Vec<WorkingDirectoryDiagnostic> {
|
) -> Vec<workspace_api::Diagnostic> {
|
||||||
diagnostics
|
diagnostics
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|diagnostic| WorkingDirectoryDiagnostic {
|
.map(|diagnostic| workspace_api::Diagnostic {
|
||||||
code: diagnostic.code,
|
code: diagnostic.code,
|
||||||
severity: match diagnostic.severity {
|
severity: match diagnostic.severity {
|
||||||
DiagnosticSeverity::Info => WorkingDirectoryDiagnosticSeverity::Info,
|
DiagnosticSeverity::Info => workspace_api::DiagnosticSeverity::Info,
|
||||||
DiagnosticSeverity::Warning => WorkingDirectoryDiagnosticSeverity::Warning,
|
DiagnosticSeverity::Warning => workspace_api::DiagnosticSeverity::Warning,
|
||||||
DiagnosticSeverity::Error => WorkingDirectoryDiagnosticSeverity::Error,
|
DiagnosticSeverity::Error => workspace_api::DiagnosticSeverity::Error,
|
||||||
},
|
},
|
||||||
message: diagnostic.message,
|
message: diagnostic.message,
|
||||||
})
|
})
|
||||||
@@ -8850,7 +8839,7 @@ async fn scoped_list_runtime_working_directories(
|
|||||||
let (items, diagnostics) = runtime_working_directory_summaries(&api, &path.runtime_id)?;
|
let (items, diagnostics) = runtime_working_directory_summaries(&api, &path.runtime_id)?;
|
||||||
Ok(Json(BrowserWorkingDirectoryListResponse {
|
Ok(Json(BrowserWorkingDirectoryListResponse {
|
||||||
workspace_id: api.config.workspace_id.clone(),
|
workspace_id: api.config.workspace_id.clone(),
|
||||||
items,
|
items: items.into_iter().map(crate::workdir_api::summary).collect(),
|
||||||
diagnostics: working_directory_diagnostics(diagnostics),
|
diagnostics: working_directory_diagnostics(diagnostics),
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
@@ -8859,7 +8848,7 @@ async fn scoped_create_runtime_working_directory(
|
|||||||
State(api): State<WorkspaceApi>,
|
State(api): State<WorkspaceApi>,
|
||||||
AxumPath(path): AxumPath<ScopedRuntimePath>,
|
AxumPath(path): AxumPath<ScopedRuntimePath>,
|
||||||
Json(request): Json<BrowserWorkingDirectoryCreateRequest>,
|
Json(request): Json<BrowserWorkingDirectoryCreateRequest>,
|
||||||
) -> ApiResult<(StatusCode, Json<BrowserWorkingDirectoryDetailResponse>)> {
|
) -> ApiResult<(StatusCode, Json<BrowserWorkingDirectoryCreateResponse>)> {
|
||||||
create_workspace_working_directory(
|
create_workspace_working_directory(
|
||||||
&api,
|
&api,
|
||||||
&path.workspace_id,
|
&path.workspace_id,
|
||||||
@@ -8893,7 +8882,7 @@ async fn scoped_list_working_directories(
|
|||||||
let items = working_directory_summaries(&api)?;
|
let items = working_directory_summaries(&api)?;
|
||||||
Ok(Json(BrowserWorkingDirectoryListResponse {
|
Ok(Json(BrowserWorkingDirectoryListResponse {
|
||||||
workspace_id: api.config.workspace_id.clone(),
|
workspace_id: api.config.workspace_id.clone(),
|
||||||
items,
|
items: items.into_iter().map(crate::workdir_api::summary).collect(),
|
||||||
diagnostics: Vec::new(),
|
diagnostics: Vec::new(),
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
@@ -8902,7 +8891,7 @@ async fn scoped_create_working_directory(
|
|||||||
State(api): State<WorkspaceApi>,
|
State(api): State<WorkspaceApi>,
|
||||||
AxumPath(path): AxumPath<ScopedWorkspacePath>,
|
AxumPath(path): AxumPath<ScopedWorkspacePath>,
|
||||||
Json(request): Json<BrowserWorkingDirectoryCreateRequest>,
|
Json(request): Json<BrowserWorkingDirectoryCreateRequest>,
|
||||||
) -> ApiResult<(StatusCode, Json<BrowserWorkingDirectoryDetailResponse>)> {
|
) -> ApiResult<(StatusCode, Json<BrowserWorkingDirectoryCreateResponse>)> {
|
||||||
create_workspace_working_directory(&api, &path.workspace_id, None, request).await
|
create_workspace_working_directory(&api, &path.workspace_id, None, request).await
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -8995,7 +8984,7 @@ async fn create_workspace_working_directory(
|
|||||||
workspace_id: &str,
|
workspace_id: &str,
|
||||||
route_runtime_id: Option<&str>,
|
route_runtime_id: Option<&str>,
|
||||||
request: BrowserWorkingDirectoryCreateRequest,
|
request: BrowserWorkingDirectoryCreateRequest,
|
||||||
) -> ApiResult<(StatusCode, Json<BrowserWorkingDirectoryDetailResponse>)> {
|
) -> ApiResult<(StatusCode, Json<BrowserWorkingDirectoryCreateResponse>)> {
|
||||||
validate_workspace_scope(api, workspace_id)?;
|
validate_workspace_scope(api, workspace_id)?;
|
||||||
if let (Some(route_runtime_id), Some(request_runtime_id)) =
|
if let (Some(route_runtime_id), Some(request_runtime_id)) =
|
||||||
(route_runtime_id, request.runtime_id.as_deref())
|
(route_runtime_id, request.runtime_id.as_deref())
|
||||||
@@ -9158,7 +9147,17 @@ async fn create_workspace_working_directory(
|
|||||||
&reserved.resolved_runtime_id,
|
&reserved.resolved_runtime_id,
|
||||||
&reserved.working_directory_id,
|
&reserved.working_directory_id,
|
||||||
)
|
)
|
||||||
.map(|response| (StatusCode::OK, response));
|
.map(|Json(response)| {
|
||||||
|
(
|
||||||
|
StatusCode::OK,
|
||||||
|
Json(BrowserWorkingDirectoryCreateResponse {
|
||||||
|
workspace_id: response.workspace_id,
|
||||||
|
runtime_id: response.runtime_id,
|
||||||
|
item: response.item,
|
||||||
|
diagnostics: response.diagnostics,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
let runtime = match api
|
let runtime = match api
|
||||||
@@ -9337,10 +9336,10 @@ async fn create_workspace_working_directory(
|
|||||||
apply_workdir_occupancy_projection(api, &mut summary)?;
|
apply_workdir_occupancy_projection(api, &mut summary)?;
|
||||||
Ok((
|
Ok((
|
||||||
StatusCode::CREATED,
|
StatusCode::CREATED,
|
||||||
Json(BrowserWorkingDirectoryDetailResponse {
|
Json(BrowserWorkingDirectoryCreateResponse {
|
||||||
workspace_id: workspace_id.to_string(),
|
workspace_id: workspace_id.to_string(),
|
||||||
runtime_id: reserved.resolved_runtime_id,
|
runtime_id: reserved.resolved_runtime_id,
|
||||||
item: summary,
|
item: crate::workdir_api::summary(summary),
|
||||||
diagnostics: working_directory_diagnostics(result.diagnostics),
|
diagnostics: working_directory_diagnostics(result.diagnostics),
|
||||||
}),
|
}),
|
||||||
))
|
))
|
||||||
@@ -9363,7 +9362,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(),
|
||||||
runtime_id: runtime_id.to_string(),
|
runtime_id: runtime_id.to_string(),
|
||||||
item: summary,
|
item: crate::workdir_api::summary(summary),
|
||||||
diagnostics: working_directory_diagnostics(result.diagnostics),
|
diagnostics: working_directory_diagnostics(result.diagnostics),
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
@@ -9374,7 +9373,9 @@ 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(),
|
||||||
runtime_id: runtime_id.to_string(),
|
runtime_id: runtime_id.to_string(),
|
||||||
item: projected_workdir_summary_from_record(&api, &record)?,
|
item: crate::workdir_api::summary(projected_workdir_summary_from_record(
|
||||||
|
&api, &record,
|
||||||
|
)?),
|
||||||
diagnostics: working_directory_diagnostics(result.diagnostics),
|
diagnostics: working_directory_diagnostics(result.diagnostics),
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
@@ -9433,7 +9434,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(),
|
||||||
runtime_id: runtime_id.to_string(),
|
runtime_id: runtime_id.to_string(),
|
||||||
item: summary,
|
item: crate::workdir_api::summary(summary),
|
||||||
diagnostics: working_directory_diagnostics(result.diagnostics),
|
diagnostics: working_directory_diagnostics(result.diagnostics),
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
@@ -22987,6 +22988,77 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn browser_workspace_workdir_create_rejects_stale_json_before_side_effects() {
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
init_clean_git_workspace(dir.path());
|
||||||
|
let api = test_api(dir.path()).await;
|
||||||
|
let token = seed_test_api_token(api.store.as_ref(), "stale-workdir-create-json");
|
||||||
|
|
||||||
|
let response = request_json_authenticated(
|
||||||
|
build_router(api.clone()),
|
||||||
|
"POST",
|
||||||
|
&format!("/api/w/{TEST_WORKSPACE_ID}/working-directories"),
|
||||||
|
Some(serde_json::json!({
|
||||||
|
"runtime_id": "missing-runtime",
|
||||||
|
"repository_id": TEST_REPOSITORY_ID,
|
||||||
|
"selector": "HEAD",
|
||||||
|
"operation_id": "stale-workdir-create",
|
||||||
|
"path": "/tmp/legacy-workdir",
|
||||||
|
})),
|
||||||
|
&token,
|
||||||
|
StatusCode::UNPROCESSABLE_ENTITY,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
assert!(
|
||||||
|
response["message"]
|
||||||
|
.as_str()
|
||||||
|
.unwrap_or_default()
|
||||||
|
.contains("unknown field")
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
api.config_store
|
||||||
|
.load_workdir_create_operation(TEST_WORKSPACE_ID, "stale-workdir-create")
|
||||||
|
.unwrap()
|
||||||
|
.is_none()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn browser_workspace_workdir_create_rejects_unconfigured_repository() {
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
init_clean_git_workspace(dir.path());
|
||||||
|
let api = test_api(dir.path()).await;
|
||||||
|
let token = seed_test_api_token(api.store.as_ref(), "missing-workdir-repository");
|
||||||
|
|
||||||
|
let response = request_json_authenticated(
|
||||||
|
build_router(api.clone()),
|
||||||
|
"POST",
|
||||||
|
&format!("/api/w/{TEST_WORKSPACE_ID}/working-directories"),
|
||||||
|
Some(serde_json::json!({
|
||||||
|
"runtime_id": EMBEDDED_WORKER_RUNTIME_ID,
|
||||||
|
"repository_id": "foreign-or-missing-repository",
|
||||||
|
"selector": "HEAD",
|
||||||
|
"operation_id": "missing-workdir-repository",
|
||||||
|
})),
|
||||||
|
&token,
|
||||||
|
StatusCode::NOT_FOUND,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
assert!(
|
||||||
|
response["message"]
|
||||||
|
.as_str()
|
||||||
|
.unwrap_or_default()
|
||||||
|
.contains("unknown local repository")
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
api.config_store
|
||||||
|
.load_workdir_create_operation(TEST_WORKSPACE_ID, "missing-workdir-repository")
|
||||||
|
.unwrap()
|
||||||
|
.is_none()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn browser_workspace_workdir_create_delegates_and_records_default_runtime_failure() {
|
async fn browser_workspace_workdir_create_delegates_and_records_default_runtime_failure() {
|
||||||
let dir = tempfile::tempdir().unwrap();
|
let dir = tempfile::tempdir().unwrap();
|
||||||
@@ -25405,7 +25477,7 @@ VALUES ('0192f0e8-4d84-7d6e-a000-000000000001', 'ticket', 3);
|
|||||||
fn workspace_workdir_response_serializes_shared_occupied_contract() {
|
fn workspace_workdir_response_serializes_shared_occupied_contract() {
|
||||||
let response = BrowserWorkingDirectoryListResponse {
|
let response = BrowserWorkingDirectoryListResponse {
|
||||||
workspace_id: TEST_WORKSPACE_ID.to_string(),
|
workspace_id: TEST_WORKSPACE_ID.to_string(),
|
||||||
items: vec![WorkingDirectorySummary {
|
items: vec![crate::workdir_api::summary(WorkingDirectorySummary {
|
||||||
working_directory_id: "wd-1".to_string(),
|
working_directory_id: "wd-1".to_string(),
|
||||||
repository_id: "main".to_string(),
|
repository_id: "main".to_string(),
|
||||||
creation_selector: None,
|
creation_selector: None,
|
||||||
@@ -25425,7 +25497,7 @@ VALUES ('0192f0e8-4d84-7d6e-a000-000000000001', 'ticket', 3);
|
|||||||
display_name: "Coder".to_string(),
|
display_name: "Coder".to_string(),
|
||||||
linked_at: "2026-08-12T00:00:00Z".to_string(),
|
linked_at: "2026-08-12T00:00:00Z".to_string(),
|
||||||
}),
|
}),
|
||||||
}],
|
})],
|
||||||
diagnostics: vec![],
|
diagnostics: vec![],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,91 @@
|
|||||||
|
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");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
// Generated from workspace-api. Do not edit by hand.
|
||||||
|
// Regenerate: cargo run -q -p workspace-api --features typescript --example generate_workdir_api_types > web/workspace/src/lib/generated/workdir-api.ts
|
||||||
|
|
||||||
|
export type DiagnosticSeverity = "info" | "warning" | "error";
|
||||||
|
|
||||||
|
export type Diagnostic = { code: string, severity: DiagnosticSeverity, message: string, };
|
||||||
|
|
||||||
|
export type WorkingDirectoryMaterializerKind = "runtime_git_cache" | "local_git_worktree";
|
||||||
|
|
||||||
|
export type WorkingDirectoryStatusKind = "active" | "cleanup_pending" | "corrupted" | "not_found" | "unknown";
|
||||||
|
|
||||||
|
export type WorkingDirectoryCleanupTarget = { kind: string, working_directory_id: string, repository_id: string, };
|
||||||
|
|
||||||
|
export type WorkingDirectoryOccupancy = { runtime_id: string, worker_id: string, display_name: string, linked_at: string, };
|
||||||
|
|
||||||
|
export type WorkingDirectorySummary = { working_directory_id: string, repository_id: string, creation_selector?: string | null, creation_ref?: string | null, creation_tree?: string | null, current_selector?: string | null, current_ref?: string | null, current_tree?: string | null, observed_at_epoch_seconds?: number | null, materializer_kind: WorkingDirectoryMaterializerKind, cleanup_target?: WorkingDirectoryCleanupTarget | null, status: WorkingDirectoryStatusKind, cleanliness?: string | null, primary_worker_id?: string | null, occupied_by?: WorkingDirectoryOccupancy | null, };
|
||||||
|
|
||||||
|
export type WorkingDirectoryCreateRequest = { runtime_id?: string | null, repository_id: string, selector?: string | null, operation_id?: string | null, };
|
||||||
|
|
||||||
|
export type WorkingDirectoryListResponse = { workspace_id: string, items: Array<WorkingDirectorySummary>, diagnostics: Array<Diagnostic>, };
|
||||||
|
|
||||||
|
export type WorkingDirectoryDetailResponse = { workspace_id: string, runtime_id: string, item: WorkingDirectorySummary, diagnostics: Array<Diagnostic>, };
|
||||||
|
|
||||||
|
export type WorkingDirectoryCreateResponse = { workspace_id: string, runtime_id: string, item: WorkingDirectorySummary, diagnostics: Array<Diagnostic>, };
|
||||||
Reference in New Issue
Block a user