fix: share workspace runtime worker contracts

This commit is contained in:
2026-08-21 12:35:58 +09:00
parent 72cae33ea6
commit ee8ee360ef
11 changed files with 498 additions and 217 deletions
Generated
+11
View File
@@ -540,6 +540,7 @@ dependencies = [
"tokio-tungstenite 0.29.0", "tokio-tungstenite 0.29.0",
"uuid", "uuid",
"workdir", "workdir",
"workspace-api",
] ]
[[package]] [[package]]
@@ -6135,6 +6136,15 @@ dependencies = [
"worker", "worker",
] ]
[[package]]
name = "workspace-api"
version = "0.1.0"
dependencies = [
"serde",
"serde_json",
"workdir",
]
[[package]] [[package]]
name = "writeable" name = "writeable"
version = "0.6.3" version = "0.6.3"
@@ -6265,6 +6275,7 @@ dependencies = [
"workdir", "workdir",
"worker", "worker",
"worker-runtime", "worker-runtime",
"workspace-api",
] ]
[[package]] [[package]]
+3
View File
@@ -27,6 +27,7 @@ members = [
"crates/ticket", "crates/ticket",
"crates/merge-request", "crates/merge-request",
"crates/project-record", "crates/project-record",
"crates/workspace-api",
"crates/workspace-server", "crates/workspace-server",
"tests/e2e", "tests/e2e",
] ]
@@ -57,6 +58,7 @@ default-members = [
"crates/ticket", "crates/ticket",
"crates/merge-request", "crates/merge-request",
"crates/project-record", "crates/project-record",
"crates/workspace-api",
"crates/workspace-server", "crates/workspace-server",
] ]
@@ -78,6 +80,7 @@ ticket = { path = "crates/ticket" }
project-record = { path = "crates/project-record" } project-record = { path = "crates/project-record" }
worker = { path = "crates/worker" } worker = { path = "crates/worker" }
worker-runtime = { path = "crates/worker-runtime" } worker-runtime = { path = "crates/worker-runtime" }
workspace-api = { path = "crates/workspace-api" }
yoi-plugin-pdk = { path = "crates/plugin-pdk" } yoi-plugin-pdk = { path = "crates/plugin-pdk" }
yoi = { path = "crates/yoi" } yoi = { path = "crates/yoi" }
protocol = { path = "crates/protocol" } protocol = { path = "crates/protocol" }
+1
View File
@@ -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 }
workspace-api.workspace = true
workdir = { workspace = true } workdir = { workspace = true }
[dev-dependencies] [dev-dependencies]
+10 -98
View File
@@ -1,13 +1,21 @@
use futures::{SinkExt, StreamExt}; use futures::{SinkExt, StreamExt};
use protocol::stream::{decode_event, encode_method}; use protocol::stream::{decode_event, encode_method};
use protocol::{ErrorCode, Event, Method}; use protocol::{ErrorCode, Event, Method};
use serde::Deserialize;
use std::collections::VecDeque; use std::collections::VecDeque;
use std::fmt; 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; pub use workdir::workspace::WorkingDirectorySummary as BackendWorkingDirectorySummary;
pub use workspace_api::{
Diagnostic as BackendDiagnostic, DiagnosticSeverity as BackendDiagnosticSeverity,
ListResponse as BackendRuntimeListResponse, RuntimeSummary as BackendRuntimeSummary,
WorkerCapabilitySummary as BackendWorkerCapabilitySummary,
WorkerImplementationSummary as BackendWorkerImplementationSummary,
WorkerRestoreResponse as BackendWorkerRestoreResponse,
WorkerRestoreResult as BackendWorkerRestoreResult, WorkerSummary as BackendWorkerSummary,
WorkerWorkspaceSummary as BackendWorkerWorkspaceSummary,
};
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
pub struct BackendRuntimeTarget { pub struct BackendRuntimeTarget {
@@ -93,94 +101,6 @@ impl BackendRuntimeListTarget {
} }
} }
#[derive(Debug, Clone, Deserialize)]
pub struct BackendRuntimeListResponse<T> {
pub workspace_id: String,
pub limit: usize,
pub items: Vec<T>,
pub source: String,
#[serde(default)]
pub diagnostics: Vec<BackendDiagnostic>,
}
#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
pub struct BackendRuntimeSummary {
pub runtime_id: String,
pub label: String,
pub kind: String,
pub status: String,
#[serde(default)]
pub host_ids: Vec<String>,
#[serde(default)]
pub diagnostics: Vec<BackendDiagnostic>,
}
#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
pub struct BackendWorkerWorkspaceSummary {
pub visibility: String,
pub identity: String,
}
#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
pub struct BackendWorkerImplementationSummary {
pub kind: String,
pub display_hint: String,
}
#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
pub struct BackendWorkerCapabilitySummary {
pub can_stop: bool,
pub can_spawn_followup: bool,
}
#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
pub struct BackendWorkerSummary {
pub runtime_id: String,
pub worker_id: String,
pub resource_key: String,
pub host_id: String,
#[serde(default)]
pub display_name: String,
pub label: String,
#[serde(default)]
pub profile: Option<String>,
#[serde(default)]
pub singleton_key: Option<String>,
#[serde(default)]
pub tags: Vec<String>,
pub workspace: BackendWorkerWorkspaceSummary,
pub state: String,
#[serde(default)]
pub last_seen_at: Option<String>,
#[serde(default)]
pub pinned: bool,
#[serde(default)]
pub retention_state: String,
pub implementation: BackendWorkerImplementationSummary,
pub capabilities: BackendWorkerCapabilitySummary,
#[serde(default)]
pub working_directory: Option<BackendWorkingDirectorySummary>,
#[serde(default)]
pub diagnostics: Vec<BackendDiagnostic>,
}
#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
pub struct BackendWorkerRestoreResult {
pub state: String,
#[serde(default)]
pub worker: Option<BackendWorkerSummary>,
#[serde(default)]
pub diagnostics: Vec<BackendDiagnostic>,
}
#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
pub struct BackendWorkerRestoreResponse {
pub workspace_id: String,
pub runtime_id: String,
pub worker_id: String,
pub result: BackendWorkerRestoreResult,
}
#[derive(Debug)] #[derive(Debug)]
pub struct BackendRuntimeClient { pub struct BackendRuntimeClient {
target: BackendRuntimeTarget, target: BackendRuntimeTarget,
@@ -277,7 +197,7 @@ pub async fn list_backend_workers(
} }
Err(error) => diagnostics.push(BackendDiagnostic { Err(error) => diagnostics.push(BackendDiagnostic {
code: "runtime_worker_list_failed".to_string(), code: "runtime_worker_list_failed".to_string(),
severity: Some("error".to_string()), severity: BackendDiagnosticSeverity::Error,
message: format!( message: format!(
"failed to list workers for runtime {}: {error}", "failed to list workers for runtime {}: {error}",
runtime.runtime_id runtime.runtime_id
@@ -619,14 +539,6 @@ fn percent_encode(input: &str, keep: impl Fn(u8) -> bool) -> String {
encoded encoded
} }
#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
pub struct BackendDiagnostic {
pub code: String,
#[serde(default)]
pub severity: Option<String>,
pub message: String,
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
+3 -3
View File
@@ -22,9 +22,9 @@ pub use backend_auth::{
poll_device_login, start_device_login, wait_for_device_login, poll_device_login, start_device_login, wait_for_device_login,
}; };
pub use backend_runtime::{ pub use backend_runtime::{
BackendDiagnostic, BackendRuntimeClient, BackendRuntimeClientError, BackendRuntimeListResponse, BackendDiagnostic, BackendDiagnosticSeverity, BackendRuntimeClient, BackendRuntimeClientError,
BackendRuntimeListTarget, BackendRuntimeSummary, BackendRuntimeTarget, BackendRuntimeListResponse, BackendRuntimeListTarget, BackendRuntimeSummary,
BackendWorkerCapabilitySummary, BackendWorkerImplementationSummary, BackendRuntimeTarget, BackendWorkerCapabilitySummary, BackendWorkerImplementationSummary,
BackendWorkerRestoreResponse, BackendWorkerRestoreResult, BackendWorkerSummary, BackendWorkerRestoreResponse, BackendWorkerRestoreResult, BackendWorkerSummary,
BackendWorkerWorkspaceSummary, BackendWorkingDirectorySummary, list_backend_stopped_workers, BackendWorkerWorkspaceSummary, BackendWorkingDirectorySummary, list_backend_stopped_workers,
list_backend_workers, restore_backend_worker, list_backend_workers, restore_backend_worker,
+2 -1
View File
@@ -46,7 +46,7 @@ pub(crate) async fn run(
} }
Err(error) => response.diagnostics.push(client::BackendDiagnostic { Err(error) => response.diagnostics.push(client::BackendDiagnostic {
code: "backend_stopped_workers_list_failed".to_string(), code: "backend_stopped_workers_list_failed".to_string(),
severity: Some("error".to_string()), severity: client::BackendDiagnosticSeverity::Error,
message: error.to_string(), message: error.to_string(),
}), }),
} }
@@ -396,6 +396,7 @@ mod tests {
workspace: BackendWorkerWorkspaceSummary { workspace: BackendWorkerWorkspaceSummary {
visibility: "workspace".to_string(), visibility: "workspace".to_string(),
identity: "ws".to_string(), identity: "ws".to_string(),
workspace_id: Some("ws".to_string()),
}, },
state: "running".to_string(), state: "running".to_string(),
last_seen_at: None, last_seen_at: None,
+13
View File
@@ -0,0 +1,13 @@
[package]
name = "workspace-api"
version = "0.1.0"
edition.workspace = true
license.workspace = true
publish = false
[dependencies]
serde = { workspace = true, features = ["derive"] }
workdir.workspace = true
[dev-dependencies]
serde_json.workspace = true
+195
View File
@@ -0,0 +1,195 @@
//! Shared Workspace HTTP resource contracts.
//!
//! This crate owns transport DTOs exposed by the Workspace Server and consumed
//! by Rust clients. Runtime-internal projections remain in their owning crates;
//! callers must explicitly construct these Workspace-authoritative resources.
use serde::{Deserialize, Serialize};
use workdir::workspace::WorkingDirectorySummary;
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum DiagnosticSeverity {
Info,
Warning,
Error,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct Diagnostic {
pub code: String,
pub severity: DiagnosticSeverity,
pub message: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct ListResponse<T> {
pub workspace_id: String,
pub limit: usize,
pub items: Vec<T>,
pub source: String,
#[serde(default)]
pub diagnostics: Vec<Diagnostic>,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum RuntimeSourceKind {
EmbeddedWorkerRuntime,
RemoteHttp,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum RuntimeSourceStatus {
Active,
Reserved,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum RuntimeIdentityAuthority {
RuntimeRegistryProjection,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct RuntimeSourceSummary {
pub kind: RuntimeSourceKind,
pub status: RuntimeSourceStatus,
pub identity_authority: RuntimeIdentityAuthority,
pub note: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct RuntimeCapabilitySummary {
pub can_list_hosts: bool,
pub can_list_workers: bool,
pub can_get_worker: bool,
pub can_spawn_worker: bool,
pub can_stop_worker: bool,
pub has_workspace_fs: bool,
pub has_shell: bool,
pub has_git: bool,
pub supports_worktrees: bool,
pub supports_backend_internal_tools: bool,
pub workspace_scope: String,
pub max_workers: usize,
pub os: String,
pub arch: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct RuntimeSummary {
pub runtime_id: String,
pub label: String,
pub kind: String,
pub status: String,
pub source: RuntimeSourceSummary,
#[serde(default)]
pub host_ids: Vec<String>,
pub capabilities: RuntimeCapabilitySummary,
#[serde(default)]
pub diagnostics: Vec<Diagnostic>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct WorkerWorkspaceSummary {
pub visibility: String,
pub identity: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub workspace_id: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct WorkerImplementationSummary {
pub kind: String,
pub display_hint: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct WorkerCapabilitySummary {
pub can_stop: bool,
pub can_spawn_followup: bool,
}
/// Workspace-authoritative Worker projection.
///
/// `resource_key` is required here even though Runtime-internal Worker summaries
/// do not carry one. The Workspace Server must resolve it from Workspace
/// authority before constructing this response.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct WorkerSummary {
pub runtime_id: String,
pub worker_id: String,
pub resource_key: String,
pub host_id: String,
#[serde(default)]
pub display_name: String,
pub label: String,
pub profile: Option<String>,
pub singleton_key: Option<String>,
#[serde(default)]
pub tags: Vec<String>,
pub workspace: WorkerWorkspaceSummary,
pub state: String,
pub last_seen_at: Option<String>,
#[serde(default)]
pub pinned: bool,
#[serde(default)]
pub retention_state: String,
pub implementation: WorkerImplementationSummary,
pub capabilities: WorkerCapabilitySummary,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub working_directory: Option<WorkingDirectorySummary>,
#[serde(default)]
pub diagnostics: Vec<Diagnostic>,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum WorkerOperationState {
Accepted,
Unsupported,
Rejected,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct WorkerRestoreResult {
pub state: WorkerOperationState,
pub worker: Option<WorkerSummary>,
#[serde(default)]
pub diagnostics: Vec<Diagnostic>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct WorkerRestoreResponse {
pub workspace_id: String,
pub runtime_id: String,
pub worker_id: String,
pub result: WorkerRestoreResult,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn worker_resource_key_is_required() {
let payload = serde_json::json!({
"runtime_id": "arcadia",
"worker_id": "worker-1",
"host_id": "host",
"display_name": "Coder",
"label": "Coder",
"workspace": {
"visibility": "workspace",
"identity": "workspace-test"
},
"state": "idle",
"implementation": {"kind": "worker", "display_hint": "Coder"},
"capabilities": {"can_stop": true, "can_spawn_followup": false}
});
assert!(serde_json::from_value::<WorkerSummary>(payload).is_err());
}
}
+1
View File
@@ -38,6 +38,7 @@ tokio = { workspace = true, features = ["fs", "macros", "net", "rt-multi-thread"
tower.workspace = true tower.workspace = true
tokio-tungstenite.workspace = true tokio-tungstenite.workspace = true
worker.workspace = true worker.workspace = true
workspace-api.workspace = true
workdir = { workspace = true, features = ["http-client"] } workdir = { workspace = true, features = ["http-client"] }
worker-runtime.workspace = true worker-runtime.workspace = true
toml.workspace = true toml.workspace = true
+123 -8
View File
@@ -246,8 +246,6 @@ pub struct WorkerCapabilitySummary {
pub struct WorkerSummary { pub struct WorkerSummary {
#[serde(flatten)] #[serde(flatten)]
pub worker: RuntimeWorkerRef, pub worker: RuntimeWorkerRef,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub resource_key: Option<String>,
pub host_id: String, pub host_id: String,
/// Human-readable display name. This is not identity and may be duplicated. /// Human-readable display name. This is not identity and may be duplicated.
pub display_name: String, pub display_name: String,
@@ -271,6 +269,119 @@ pub struct WorkerSummary {
pub diagnostics: Vec<RuntimeDiagnostic>, pub diagnostics: Vec<RuntimeDiagnostic>,
} }
impl From<RuntimeDiagnostic> for workspace_api::Diagnostic {
fn from(diagnostic: RuntimeDiagnostic) -> Self {
let severity = match diagnostic.severity {
DiagnosticSeverity::Info => workspace_api::DiagnosticSeverity::Info,
DiagnosticSeverity::Warning => workspace_api::DiagnosticSeverity::Warning,
DiagnosticSeverity::Error => workspace_api::DiagnosticSeverity::Error,
};
Self {
code: diagnostic.code,
severity,
message: diagnostic.message,
}
}
}
impl From<RuntimeSourceSummary> for workspace_api::RuntimeSourceSummary {
fn from(source: RuntimeSourceSummary) -> Self {
let kind = match source.kind {
RuntimeSourceKind::EmbeddedWorkerRuntime => {
workspace_api::RuntimeSourceKind::EmbeddedWorkerRuntime
}
RuntimeSourceKind::RemoteHttp => workspace_api::RuntimeSourceKind::RemoteHttp,
};
let status = match source.status {
RuntimeSourceStatus::Active => workspace_api::RuntimeSourceStatus::Active,
RuntimeSourceStatus::Reserved => workspace_api::RuntimeSourceStatus::Reserved,
};
let identity_authority = match source.identity_authority {
RuntimeIdentityAuthority::RuntimeRegistryProjection => {
workspace_api::RuntimeIdentityAuthority::RuntimeRegistryProjection
}
};
Self {
kind,
status,
identity_authority,
note: source.note,
}
}
}
impl From<RuntimeCapabilitySummary> for workspace_api::RuntimeCapabilitySummary {
fn from(capabilities: RuntimeCapabilitySummary) -> Self {
Self {
can_list_hosts: capabilities.can_list_hosts,
can_list_workers: capabilities.can_list_workers,
can_get_worker: capabilities.can_get_worker,
can_spawn_worker: capabilities.can_spawn_worker,
can_stop_worker: capabilities.can_stop_worker,
has_workspace_fs: capabilities.has_workspace_fs,
has_shell: capabilities.has_shell,
has_git: capabilities.has_git,
supports_worktrees: capabilities.supports_worktrees,
supports_backend_internal_tools: capabilities.supports_backend_internal_tools,
workspace_scope: capabilities.workspace_scope,
max_workers: capabilities.max_workers,
os: capabilities.os,
arch: capabilities.arch,
}
}
}
impl From<RuntimeSummary> for workspace_api::RuntimeSummary {
fn from(runtime: RuntimeSummary) -> Self {
Self {
runtime_id: runtime.runtime_id,
label: runtime.label,
kind: runtime.kind,
status: runtime.status,
source: runtime.source.into(),
host_ids: runtime.host_ids,
capabilities: runtime.capabilities.into(),
diagnostics: runtime.diagnostics.into_iter().map(Into::into).collect(),
}
}
}
pub(crate) fn workspace_worker_summary(
summary: WorkerSummary,
resource_key: String,
) -> workspace_api::WorkerSummary {
workspace_api::WorkerSummary {
runtime_id: summary.worker.runtime_id,
worker_id: summary.worker.worker_id,
resource_key,
host_id: summary.host_id,
display_name: summary.display_name,
label: summary.label,
profile: summary.profile,
singleton_key: summary.singleton_key,
tags: summary.tags,
workspace: workspace_api::WorkerWorkspaceSummary {
visibility: summary.workspace.visibility,
identity: summary.workspace.identity,
workspace_id: summary.workspace.workspace_id,
},
state: summary.state,
last_seen_at: summary.last_seen_at,
pinned: summary.pinned,
retention_state: summary.retention_state,
implementation: workspace_api::WorkerImplementationSummary {
kind: summary.implementation.kind,
display_hint: summary.implementation.display_hint,
},
capabilities: workspace_api::WorkerCapabilitySummary {
can_stop: summary.capabilities.can_stop,
can_spawn_followup: summary.capabilities.can_spawn_followup,
},
working_directory: summary.working_directory,
diagnostics: summary.diagnostics.into_iter().map(Into::into).collect(),
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct WorkerRestoreResult { pub struct WorkerRestoreResult {
pub state: WorkerOperationState, pub state: WorkerOperationState,
@@ -509,6 +620,16 @@ pub enum WorkerOperationState {
Rejected, Rejected,
} }
impl From<WorkerOperationState> for workspace_api::WorkerOperationState {
fn from(state: WorkerOperationState) -> Self {
match state {
WorkerOperationState::Accepted => Self::Accepted,
WorkerOperationState::Unsupported => Self::Unsupported,
WorkerOperationState::Rejected => Self::Rejected,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct WorkerSpawnAcceptanceEvidence { pub struct WorkerSpawnAcceptanceEvidence {
pub kind: String, pub kind: String,
@@ -1680,7 +1801,6 @@ impl EmbeddedWorkerRuntime {
); );
WorkerSummary { WorkerSummary {
worker: RuntimeWorkerRef::new(&self.runtime_id, worker_id.clone()), worker: RuntimeWorkerRef::new(&self.runtime_id, worker_id.clone()),
resource_key: None,
host_id: self.host_id.clone(), host_id: self.host_id.clone(),
display_name: display.display_name.clone(), display_name: display.display_name.clone(),
label: display.display_name, label: display.display_name,
@@ -1720,7 +1840,6 @@ impl EmbeddedWorkerRuntime {
); );
WorkerSummary { WorkerSummary {
worker: RuntimeWorkerRef::new(&self.runtime_id, worker_id.clone()), worker: RuntimeWorkerRef::new(&self.runtime_id, worker_id.clone()),
resource_key: None,
host_id: self.host_id.clone(), host_id: self.host_id.clone(),
display_name: display.display_name.clone(), display_name: display.display_name.clone(),
label: display.display_name, label: display.display_name,
@@ -2806,7 +2925,6 @@ impl RemoteWorkerRuntime {
); );
WorkerSummary { WorkerSummary {
worker: RuntimeWorkerRef::new(&self.runtime_id, worker_id.clone()), worker: RuntimeWorkerRef::new(&self.runtime_id, worker_id.clone()),
resource_key: None,
host_id: self.host_id.clone(), host_id: self.host_id.clone(),
display_name: display.display_name.clone(), display_name: display.display_name.clone(),
label: display.display_name, label: display.display_name,
@@ -2850,7 +2968,6 @@ impl RemoteWorkerRuntime {
); );
WorkerSummary { WorkerSummary {
worker: RuntimeWorkerRef::new(&self.runtime_id, worker_id.clone()), worker: RuntimeWorkerRef::new(&self.runtime_id, worker_id.clone()),
resource_key: None,
host_id: self.host_id.clone(), host_id: self.host_id.clone(),
display_name: display.display_name.clone(), display_name: display.display_name.clone(),
label: display.display_name, label: display.display_name,
@@ -4222,7 +4339,6 @@ pub fn placeholder_worker(host_id: impl Into<String>) -> WorkerSummary {
let host_id = host_id.into(); let host_id = host_id.into();
WorkerSummary { WorkerSummary {
worker: RuntimeWorkerRef::new("placeholder", "worker-placeholder"), worker: RuntimeWorkerRef::new("placeholder", "worker-placeholder"),
resource_key: None,
host_id, host_id,
display_name: "Worker runtime actions are not implemented".to_string(), display_name: "Worker runtime actions are not implemented".to_string(),
label: "Worker runtime actions are not implemented".to_string(), label: "Worker runtime actions are not implemented".to_string(),
@@ -4616,7 +4732,6 @@ mod tests {
host_id: host_id.to_string(), host_id: host_id.to_string(),
workers: vec![WorkerSummary { workers: vec![WorkerSummary {
worker: RuntimeWorkerRef::new(runtime_id, worker_id), worker: RuntimeWorkerRef::new(runtime_id, worker_id),
resource_key: None,
host_id: host_id.to_string(), host_id: host_id.to_string(),
display_name: label.to_string(), display_name: label.to_string(),
label: label.to_string(), label: label.to_string(),
+136 -107
View File
@@ -77,13 +77,13 @@ use crate::hosts::{
ConfigBundleCheckResult, ConfigBundleSyncResult, DiagnosticSeverity, EMBEDDED_RUNTIME_ID, ConfigBundleCheckResult, ConfigBundleSyncResult, DiagnosticSeverity, EMBEDDED_RUNTIME_ID,
EmbeddedWorkerRuntime, HostSummary, RemoteRuntimeConfig, RemoteWorkerRuntime, EmbeddedWorkerRuntime, HostSummary, RemoteRuntimeConfig, RemoteWorkerRuntime,
RuntimeDiagnostic, RuntimeRegistry, RuntimeRegistryError, RuntimeRegistryUnregisterResult, RuntimeDiagnostic, RuntimeRegistry, RuntimeRegistryError, RuntimeRegistryUnregisterResult,
RuntimeSummary, TicketWorkerRole, WorkerCapabilitySummary, WorkerCompletionsRequest, TicketWorkerRole, WorkerCapabilitySummary, WorkerCompletionsRequest, WorkerCompletionsResult,
WorkerCompletionsResult, WorkerControlOperation, WorkerCreateBinding, WorkerControlOperation, WorkerCreateBinding, WorkerImplementationSummary, WorkerInputKind,
WorkerImplementationSummary, WorkerInputKind, WorkerInputRequest, WorkerInputResult, WorkerInputRequest, WorkerInputResult, WorkerLifecycleRequest, WorkerLifecycleResult,
WorkerLifecycleRequest, WorkerLifecycleResult, WorkerOperationState, WorkerRestoreResult, WorkerOperationState, WorkerRestoreResult, WorkerSpawnAcceptanceRequirement, WorkerSpawnIntent,
WorkerSpawnAcceptanceRequirement, WorkerSpawnIntent, WorkerSpawnRequest, WorkerSpawnResult, WorkerSpawnRequest, WorkerSpawnResult, WorkerSpawnWorkingDirectoryRequest, WorkerSummary,
WorkerSpawnWorkingDirectoryRequest, WorkerSummary, WorkerTicketAssignmentRequest, WorkerTicketAssignmentRequest, WorkerWorkspaceSummary, worker_spawn_create_fingerprint,
WorkerWorkspaceSummary, worker_spawn_create_fingerprint, workspace_worker_summary,
}; };
use crate::identity::WorkspaceIdentity; use crate::identity::WorkspaceIdentity;
use crate::memory_backend::execute_memory_backend_operation_with_authority; use crate::memory_backend::execute_memory_backend_operation_with_authority;
@@ -2264,14 +2264,6 @@ enum RuntimeWorkersStatusFilter {
Stopped, Stopped,
} }
#[derive(Debug, Serialize, Deserialize)]
pub struct WorkerRestoreResponse {
pub workspace_id: String,
#[serde(flatten)]
pub worker_ref: RuntimeWorkerRef,
pub result: WorkerRestoreResult,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")] #[serde(rename_all = "snake_case")]
pub enum CleanupTargetKind { pub enum CleanupTargetKind {
@@ -6534,7 +6526,7 @@ async fn scoped_get_profile_source_archive(
async fn scoped_list_runtimes( async fn scoped_list_runtimes(
State(api): State<WorkspaceApi>, State(api): State<WorkspaceApi>,
AxumPath(path): AxumPath<ScopedWorkspacePath>, AxumPath(path): AxumPath<ScopedWorkspacePath>,
) -> ApiResult<Json<RuntimeListResponse<RuntimeSummary>>> { ) -> ApiResult<Json<workspace_api::ListResponse<workspace_api::RuntimeSummary>>> {
validate_workspace_scope(&api, &path.workspace_id)?; validate_workspace_scope(&api, &path.workspace_id)?;
list_runtimes(State(api)).await list_runtimes(State(api)).await
} }
@@ -6722,7 +6714,7 @@ async fn scoped_worker_remove_source_boundary(
async fn scoped_get_workspace_worker( async fn scoped_get_workspace_worker(
State(api): State<WorkspaceApi>, State(api): State<WorkspaceApi>,
AxumPath(path): AxumPath<ScopedWorkspaceWorkerReferencePath>, AxumPath(path): AxumPath<ScopedWorkspaceWorkerReferencePath>,
) -> ApiResult<Json<WorkerSummary>> { ) -> ApiResult<Json<workspace_api::WorkerSummary>> {
validate_workspace_scope(&api, &path.workspace_id)?; validate_workspace_scope(&api, &path.workspace_id)?;
let worker_id = api let worker_id = api
.store .store
@@ -6738,7 +6730,7 @@ async fn scoped_get_workspace_worker(
workers workers
.items .items
.into_iter() .into_iter()
.find(|worker| worker.worker.worker_id == worker_id) .find(|worker| worker.worker_id == worker_id)
.map(Json) .map(Json)
.ok_or_else(|| { .ok_or_else(|| {
Error::UnknownWorker { Error::UnknownWorker {
@@ -6751,7 +6743,7 @@ async fn scoped_get_workspace_worker(
async fn scoped_list_workers( async fn scoped_list_workers(
State(api): State<WorkspaceApi>, State(api): State<WorkspaceApi>,
AxumPath(path): AxumPath<ScopedWorkspacePath>, AxumPath(path): AxumPath<ScopedWorkspacePath>,
) -> ApiResult<Json<RuntimeListResponse<WorkerSummary>>> { ) -> ApiResult<Json<workspace_api::ListResponse<workspace_api::WorkerSummary>>> {
validate_workspace_scope(&api, &path.workspace_id)?; validate_workspace_scope(&api, &path.workspace_id)?;
list_workers(State(api)).await list_workers(State(api)).await
} }
@@ -7010,7 +7002,7 @@ async fn restore_known_worker(
State(api): State<WorkspaceApi>, State(api): State<WorkspaceApi>,
AxumPath(path): AxumPath<ScopedRuntimeWorkerPath>, AxumPath(path): AxumPath<ScopedRuntimeWorkerPath>,
headers: HeaderMap, headers: HeaderMap,
) -> ApiResult<Json<WorkerRestoreResponse>> { ) -> ApiResult<Json<workspace_api::WorkerRestoreResponse>> {
validate_workspace_scope(&api, &path.workspace_id)?; validate_workspace_scope(&api, &path.workspace_id)?;
let source = authenticate_worker_mutation_source(&api, &path.workspace_id, &headers)?; let source = authenticate_worker_mutation_source(&api, &path.workspace_id, &headers)?;
let subject = path.worker.clone(); let subject = path.worker.clone();
@@ -7590,7 +7582,7 @@ fn build_runtime_cleanup_plan(
.items .items
.iter() .iter()
.filter(|worker| worker.state == "running") .filter(|worker| worker.state == "running")
.map(|worker| worker.worker.clone()) .map(|worker| RuntimeWorkerRef::new(&worker.runtime_id, &worker.worker_id))
.collect(); .collect();
let (workdir_summaries, mut diagnostics) = let (workdir_summaries, mut diagnostics) =
match runtime_working_directory_summaries(api, runtime_id) { match runtime_working_directory_summaries(api, runtime_id) {
@@ -8107,7 +8099,7 @@ async fn scoped_list_runtime_workers(
State(api): State<WorkspaceApi>, State(api): State<WorkspaceApi>,
AxumPath(path): AxumPath<ScopedRuntimePath>, AxumPath(path): AxumPath<ScopedRuntimePath>,
Query(query): Query<RuntimeWorkersQuery>, Query(query): Query<RuntimeWorkersQuery>,
) -> ApiResult<Json<RuntimeListResponse<WorkerSummary>>> { ) -> ApiResult<Json<workspace_api::ListResponse<workspace_api::WorkerSummary>>> {
validate_workspace_scope(&api, &path.workspace_id)?; validate_workspace_scope(&api, &path.workspace_id)?;
list_runtime_workers(State(api), AxumPath(path.runtime_id), Query(query)).await list_runtime_workers(State(api), AxumPath(path.runtime_id), Query(query)).await
} }
@@ -8166,7 +8158,7 @@ async fn scoped_restore_runtime_worker(
State(api): State<WorkspaceApi>, State(api): State<WorkspaceApi>,
AxumPath(path): AxumPath<ScopedRuntimeWorkerPath>, AxumPath(path): AxumPath<ScopedRuntimeWorkerPath>,
Query(query): Query<RestoreTicketAssignmentQuery>, Query(query): Query<RestoreTicketAssignmentQuery>,
) -> ApiResult<Json<WorkerRestoreResponse>> { ) -> ApiResult<Json<workspace_api::WorkerRestoreResponse>> {
validate_workspace_scope(&api, &path.workspace_id)?; validate_workspace_scope(&api, &path.workspace_id)?;
let workspace_id = path.workspace_id.clone(); let workspace_id = path.workspace_id.clone();
let runtime_id = path.worker.runtime_id.clone(); let runtime_id = path.worker.runtime_id.clone();
@@ -8219,11 +8211,13 @@ async fn scoped_restore_runtime_worker(
.into()); .into());
} }
assign_ticket_worker_from_lifecycle(&api, assignment, &runtime_id, &worker_id)?; assign_ticket_worker_from_lifecycle(&api, assignment, &runtime_id, &worker_id)?;
return Ok(Json(WorkerRestoreResponse { let worker = project_workspace_worker(&api, worker)?;
return Ok(Json(workspace_api::WorkerRestoreResponse {
workspace_id, workspace_id,
worker_ref: RuntimeWorkerRef::new(&runtime_id, &worker_id), runtime_id: runtime_id.clone(),
result: crate::hosts::WorkerRestoreResult { worker_id: worker_id.clone(),
state: WorkerOperationState::Accepted, result: workspace_api::WorkerRestoreResult {
state: workspace_api::WorkerOperationState::Accepted,
worker: Some(worker), worker: Some(worker),
diagnostics: Vec::new(), diagnostics: Vec::new(),
}, },
@@ -8352,7 +8346,7 @@ async fn scoped_worker_protocol_ws(
async fn scoped_list_host_workers( async fn scoped_list_host_workers(
State(api): State<WorkspaceApi>, State(api): State<WorkspaceApi>,
AxumPath(path): AxumPath<ScopedHostPath>, AxumPath(path): AxumPath<ScopedHostPath>,
) -> ApiResult<Json<RuntimeListResponse<WorkerSummary>>> { ) -> ApiResult<Json<workspace_api::ListResponse<workspace_api::WorkerSummary>>> {
validate_workspace_scope(&api, &path.workspace_id)?; validate_workspace_scope(&api, &path.workspace_id)?;
list_host_workers(State(api), AxumPath(path.host_id)).await list_host_workers(State(api), AxumPath(path.host_id)).await
} }
@@ -9304,21 +9298,21 @@ async fn list_hosts(
async fn list_runtimes( async fn list_runtimes(
State(api): State<WorkspaceApi>, State(api): State<WorkspaceApi>,
) -> ApiResult<Json<RuntimeListResponse<RuntimeSummary>>> { ) -> ApiResult<Json<workspace_api::ListResponse<workspace_api::RuntimeSummary>>> {
let limit = api.config.max_records.min(200); let limit = api.config.max_records.min(200);
let runtimes = api.runtime.list_runtimes(limit); let runtimes = api.runtime.list_runtimes(limit);
Ok(Json(RuntimeListResponse { Ok(Json(workspace_api::ListResponse {
workspace_id: api.config.workspace_id, workspace_id: api.config.workspace_id,
limit, limit,
items: runtimes.items, items: runtimes.items.into_iter().map(Into::into).collect(),
source: "worker_runtime_registry".to_string(), source: "worker_runtime_registry".to_string(),
diagnostics: runtimes.diagnostics, diagnostics: runtimes.diagnostics.into_iter().map(Into::into).collect(),
})) }))
} }
async fn list_workers( async fn list_workers(
State(api): State<WorkspaceApi>, State(api): State<WorkspaceApi>,
) -> ApiResult<Json<RuntimeListResponse<WorkerSummary>>> { ) -> ApiResult<Json<workspace_api::ListResponse<workspace_api::WorkerSummary>>> {
workers_response(api).map(Json) workers_response(api).map(Json)
} }
@@ -10025,7 +10019,7 @@ async fn post_companion_cancel(
#[derive(Debug, Serialize)] #[derive(Debug, Serialize)]
struct WorkerShowProjection { struct WorkerShowProjection {
#[serde(flatten)] #[serde(flatten)]
worker: WorkerSummary, worker: workspace_api::WorkerSummary,
updated_at: String, updated_at: String,
} }
@@ -10071,31 +10065,18 @@ async fn get_runtime_worker(
.store .store
.list_workdir_registry(&api.config.workspace_id, 500)?; .list_workdir_registry(&api.config.workspace_id, 500)?;
let updated_at = record.updated_at.clone(); let updated_at = record.updated_at.clone();
let mut worker = merge_worker_registry_projection(Some(&worker), &record, links, &workdirs); let worker = merge_worker_registry_projection(Some(&worker), &record, links, &workdirs);
worker.resource_key = Some( let worker = project_workspace_worker(&api, worker)?;
api.store
.resource_key(
&api.config.workspace_id,
WorkspaceResourceKind::Worker,
&worker_ref.worker_id,
)?
.ok_or_else(|| {
Error::Store(format!(
"Workspace Worker `{}` has no resource key",
worker_ref.worker_id
))
})?,
);
Ok(Json(WorkerShowProjection { worker, updated_at })) Ok(Json(WorkerShowProjection { worker, updated_at }))
} }
async fn restore_runtime_worker( async fn restore_runtime_worker(
State(api): State<WorkspaceApi>, State(api): State<WorkspaceApi>,
AxumPath((runtime_id, worker_id)): AxumPath<(String, String)>, AxumPath((runtime_id, worker_id)): AxumPath<(String, String)>,
) -> ApiResult<Json<WorkerRestoreResponse>> { ) -> ApiResult<Json<workspace_api::WorkerRestoreResponse>> {
let worker = resolve_workspace_worker_reference(&api, &runtime_id, &worker_id)?; let worker = resolve_workspace_worker_reference(&api, &runtime_id, &worker_id)?;
let mut result = api.restore_workspace_worker(&worker)?; let result = api.restore_workspace_worker(&worker)?;
if let Some(worker) = result.worker.as_ref() { let projected_worker = if let Some(worker) = result.worker.as_ref() {
let record = sync_worker_observation(&api, worker)?; let record = sync_worker_observation(&api, worker)?;
let links = api let links = api
.store .store
@@ -10103,27 +10084,20 @@ async fn restore_runtime_worker(
let workdirs = api let workdirs = api
.store .store
.list_workdir_registry(&api.config.workspace_id, 500)?; .list_workdir_registry(&api.config.workspace_id, 500)?;
let mut summary = merge_worker_registry_projection(Some(worker), &record, links, &workdirs); let summary = merge_worker_registry_projection(Some(worker), &record, links, &workdirs);
summary.resource_key = Some( Some(project_workspace_worker(&api, summary)?)
api.store } else {
.resource_key( None
&api.config.workspace_id, };
WorkspaceResourceKind::Worker, Ok(Json(workspace_api::WorkerRestoreResponse {
&record.worker.worker_id,
)?
.ok_or_else(|| {
Error::Store(format!(
"Workspace Worker `{}` has no resource key",
record.worker.worker_id
))
})?,
);
result.worker = Some(summary);
}
Ok(Json(WorkerRestoreResponse {
workspace_id: api.workspace_id().to_string(), workspace_id: api.workspace_id().to_string(),
worker_ref: RuntimeWorkerRef::new(&runtime_id, &worker_id), runtime_id: runtime_id.clone(),
result, worker_id: worker_id.clone(),
result: workspace_api::WorkerRestoreResult {
state: result.state.into(),
worker: projected_worker,
diagnostics: result.diagnostics.into_iter().map(Into::into).collect(),
},
})) }))
} }
@@ -10179,28 +10153,33 @@ async fn list_runtime_workers(
State(api): State<WorkspaceApi>, State(api): State<WorkspaceApi>,
AxumPath(runtime_id): AxumPath<String>, AxumPath(runtime_id): AxumPath<String>,
Query(query): Query<RuntimeWorkersQuery>, Query(query): Query<RuntimeWorkersQuery>,
) -> ApiResult<Json<RuntimeListResponse<WorkerSummary>>> { ) -> ApiResult<Json<workspace_api::ListResponse<workspace_api::WorkerSummary>>> {
let limit = api.config.max_records.min(200); let limit = api.config.max_records.min(200);
let (worker_list, source) = match query.status { let (runtime_workers, source) = match query.status {
Some(RuntimeWorkersStatusFilter::Stopped) => ( Some(RuntimeWorkersStatusFilter::Stopped) => (
api.runtime api.runtime
.list_stopped_workers_for_runtime(&runtime_id, limit) .list_stopped_workers_for_runtime(&runtime_id, limit)
.map_err(|err| err.into_error())?, .map_err(|error| error.into_error())?,
"runtime_registry_stopped", "runtime_registry_stopped",
), ),
None => ( None => (
api.runtime api.runtime
.list_workers_for_runtime(&runtime_id, limit) .list_workers_for_runtime(&runtime_id, limit)
.map_err(|err| err.into_error())?, .map_err(|error| error.into_error())?,
"runtime_registry", "runtime_registry",
), ),
}; };
Ok(Json(RuntimeListResponse { let items = project_observed_workspace_workers(&api, runtime_workers.items)?;
Ok(Json(workspace_api::ListResponse {
workspace_id: api.workspace_id().to_string(), workspace_id: api.workspace_id().to_string(),
limit, limit,
items: worker_list.items, items,
source: source.to_string(), source: source.to_string(),
diagnostics: worker_list.diagnostics, diagnostics: runtime_workers
.diagnostics
.into_iter()
.map(Into::into)
.collect(),
})) }))
} }
@@ -11142,22 +11121,70 @@ fn protocol_error_event(message: impl Into<String>) -> protocol::Event {
async fn list_host_workers( async fn list_host_workers(
State(api): State<WorkspaceApi>, State(api): State<WorkspaceApi>,
AxumPath(host_id): AxumPath<String>, AxumPath(host_id): AxumPath<String>,
) -> ApiResult<Json<RuntimeListResponse<WorkerSummary>>> { ) -> ApiResult<Json<workspace_api::ListResponse<workspace_api::WorkerSummary>>> {
let limit = api.config.max_records.min(200); let limit = api.config.max_records.min(200);
let runtime_workers = api let runtime_workers = api
.runtime .runtime
.list_workers_for_host(&host_id, limit) .list_workers_for_host(&host_id, limit)
.map_err(|err| err.into_error())?; .map_err(|err| err.into_error())?;
Ok(Json(RuntimeListResponse { let items = project_observed_workspace_workers(&api, runtime_workers.items)?;
workspace_id: api.config.workspace_id, Ok(Json(workspace_api::ListResponse {
workspace_id: api.workspace_id().to_string(),
limit, limit,
items: runtime_workers.items, items,
source: "worker_runtime_registry".to_string(), source: "worker_runtime_registry".to_string(),
diagnostics: runtime_workers.diagnostics, diagnostics: runtime_workers
.diagnostics
.into_iter()
.map(Into::into)
.collect(),
})) }))
} }
fn workers_response(api: WorkspaceApi) -> ApiResult<RuntimeListResponse<WorkerSummary>> { fn project_workspace_worker(
api: &WorkspaceApi,
summary: WorkerSummary,
) -> ApiResult<workspace_api::WorkerSummary> {
let resource_key = api
.store
.resource_key(
&api.config.workspace_id,
WorkspaceResourceKind::Worker,
&summary.worker.worker_id,
)?
.ok_or_else(|| {
Error::Store(format!(
"Workspace Worker `{}` has no resource key",
summary.worker.worker_id
))
})?;
Ok(workspace_worker_summary(summary, resource_key))
}
fn project_observed_workspace_workers(
api: &WorkspaceApi,
workers: Vec<WorkerSummary>,
) -> ApiResult<Vec<workspace_api::WorkerSummary>> {
let workdirs = api
.store
.list_workdir_registry(&api.config.workspace_id, 500)?;
workers
.into_iter()
.map(|worker| {
let record = sync_worker_observation(api, &worker)?;
let links = api
.store
.list_worker_workdir_links(&api.config.workspace_id, &record.worker)?;
let summary =
merge_worker_registry_projection(Some(&worker), &record, links, &workdirs);
project_workspace_worker(api, summary)
})
.collect()
}
fn workers_response(
api: WorkspaceApi,
) -> ApiResult<workspace_api::ListResponse<workspace_api::WorkerSummary>> {
let limit = api.config.max_records.min(200); let limit = api.config.max_records.min(200);
let runtime_workers = api.runtime.list_workers(limit); let runtime_workers = api.runtime.list_workers(limit);
let mut observed = std::collections::BTreeMap::new(); let mut observed = std::collections::BTreeMap::new();
@@ -11196,34 +11223,20 @@ fn workers_response(api: WorkspaceApi) -> ApiResult<RuntimeListResponse<WorkerSu
let links = api let links = api
.store .store
.list_worker_workdir_links(&api.config.workspace_id, &record.worker)?; .list_worker_workdir_links(&api.config.workspace_id, &record.worker)?;
let mut summary = merge_worker_registry_projection( let summary = merge_worker_registry_projection(
observed.get(&record.worker), observed.get(&record.worker),
&record, &record,
links, links,
&workdir_records, &workdir_records,
); );
summary.resource_key = Some( items.push(project_workspace_worker(&api, summary)?);
api.store
.resource_key(
&api.config.workspace_id,
WorkspaceResourceKind::Worker,
&record.worker.worker_id,
)?
.ok_or_else(|| {
Error::Store(format!(
"Workspace Worker `{}` has no resource key",
record.worker.worker_id
))
})?,
);
items.push(summary);
} }
Ok(RuntimeListResponse { Ok(workspace_api::ListResponse {
workspace_id: api.config.workspace_id, workspace_id: api.config.workspace_id,
limit, limit,
items, items,
source: "backend_worker_registry".to_string(), source: "backend_worker_registry".to_string(),
diagnostics, diagnostics: diagnostics.into_iter().map(Into::into).collect(),
}) })
} }
@@ -12084,7 +12097,6 @@ fn record_worker_summary(
fn worker_summary_from_registry(record: &WorkerRegistryRecord) -> WorkerSummary { fn worker_summary_from_registry(record: &WorkerRegistryRecord) -> WorkerSummary {
WorkerSummary { WorkerSummary {
worker: record.worker.clone(), worker: record.worker.clone(),
resource_key: None,
host_id: "backend-registry".to_string(), host_id: "backend-registry".to_string(),
display_name: record.display_name.clone(), display_name: record.display_name.clone(),
label: record.display_name.clone(), label: record.display_name.clone(),
@@ -15979,11 +15991,11 @@ mod tests {
) )
.await .await
.unwrap(); .unwrap();
assert_eq!(retried_restore.worker_id, first_worker.worker.worker_id);
assert_eq!( assert_eq!(
retried_restore.worker_ref.worker_id, retried_restore.result.state,
first_worker.worker.worker_id workspace_api::WorkerOperationState::Accepted
); );
assert_eq!(retried_restore.result.state, WorkerOperationState::Accepted);
let restored_assignment = api let restored_assignment = api
.store .store
.get_current_ticket_worker_assignment(TEST_WORKSPACE_ID, &second_ticket.id) .get_current_ticket_worker_assignment(TEST_WORKSPACE_ID, &second_ticket.id)
@@ -18159,6 +18171,23 @@ mod tests {
assert_eq!(worker["profile"], "builtin:companion"); assert_eq!(worker["profile"], "builtin:companion");
assert!(worker.get("role").is_none()); assert!(worker.get("role").is_none());
assert_eq!(worker["worker_id"], created["worker_id"]); assert_eq!(worker["worker_id"], created["worker_id"]);
let resource_key = worker["resource_key"]
.as_str()
.expect("Workspace Worker list must project a resource key");
assert!(resource_key.starts_with("W-"));
let runtime_workers =
get_json(app.clone(), "/api/runtimes/embedded-worker-runtime/workers").await;
let runtime_workers = serde_json::from_value::<
workspace_api::ListResponse<workspace_api::WorkerSummary>,
>(runtime_workers)
.expect("Runtime-scoped Worker list must use the shared Workspace API contract");
assert!(
runtime_workers
.items
.iter()
.any(|worker| worker.resource_key == resource_key)
);
let detail_path = format!( let detail_path = format!(
"/api/runtimes/{}/workers/{}", "/api/runtimes/{}/workers/{}",
created["runtime_id"].as_str().unwrap(), created["runtime_id"].as_str().unwrap(),