refactor: remove worker capability summary flags

This commit is contained in:
2026-09-30 13:11:28 +09:00
parent eb222c8d62
commit c0fd476d8b
26 changed files with 281 additions and 248 deletions
+2 -3
View File
@@ -5,7 +5,6 @@ pub use server_api::{
CreateWorkspaceWorkerRequest as BackendCreateWorkerRequest, Diagnostic as BackendDiagnostic,
DiagnosticSeverity as BackendDiagnosticSeverity, ListResponse as BackendRuntimeListResponse,
RuntimeSummary as BackendRuntimeSummary,
WorkerCapabilitySummary as BackendWorkerCapabilitySummary,
WorkerImplementationSummary as BackendWorkerImplementationSummary,
WorkerLaunchOptionsResponse as BackendWorkerLaunchOptions,
WorkerLaunchProfileCandidate as BackendWorkerLaunchProfileCandidate,
@@ -869,7 +868,6 @@ mod tests {
"pinned": false,
"retention_state": "resident",
"implementation": {"kind": "embedded", "display_hint": "Embedded"},
"capabilities": {"can_stop": true, "can_spawn_followup": false},
"diagnostics": []
},
"diagnostics": []
@@ -952,10 +950,11 @@ mod tests {
"label": "Coder",
"workspace": {"visibility": "workspace", "identity": "workspace"},
"state": "idle",
"availability": "observed",
"implementation": {"kind": "worker", "display_hint": "Coder"},
"capabilities": {"can_stop": true, "can_spawn_followup": false},
"workdir_attachments": [{
"alias": "checkout",
"effective_permissions": {"read": true, "write": true, "command": true},
"working_directory": {
"working_directory_id": "wd-1",
"source": {"kind": "repository", "repository_key": "main"},
+1 -1
View File
@@ -24,7 +24,7 @@ pub use backend_runtime::{
BackendCreateWorkerRequest, BackendCreateWorkerResponse, BackendDiagnostic,
BackendDiagnosticSeverity, BackendRuntimeClientError, BackendRuntimeListResponse,
BackendRuntimeListTarget, BackendRuntimeSummary, BackendRuntimeTarget,
BackendWorkerCapabilitySummary, BackendWorkerImplementationSummary, BackendWorkerLaunchOptions,
BackendWorkerImplementationSummary, BackendWorkerLaunchOptions,
BackendWorkerLaunchProfileCandidate, BackendWorkerLaunchRuntimeOption,
BackendWorkerLaunchTarget, BackendWorkerOperationState, BackendWorkerRestoreResponse,
BackendWorkerRestoreResult, BackendWorkerRestoreState, BackendWorkerSummary,
+2 -2
View File
@@ -1029,7 +1029,7 @@ mod tests {
),
(
"200 OK",
r#"{"workspace_id":"workspace-a","runtime_id":"embedded","worker_id":"worker-1","console_href":"/w/workspace-a/workers/worker-1","worker":{"runtime_id":"embedded","worker_id":"worker-1","host_id":"embedded","display_name":"Intake","label":"worker-1","profile":"builtin:intake","singleton_key":null,"tags":[],"workspace":{"visibility":"workspace","identity":"workspace-a","workspace_id":"workspace-a"},"state":"idle","last_seen_at":null,"pinned":false,"retention_state":"active","implementation":{"kind":"runtime","display_hint":"Runtime Worker"},"capabilities":{"can_stop":true,"can_spawn_followup":false},"diagnostics":[]},"diagnostics":[]}"#,
r#"{"workspace_id":"workspace-a","runtime_id":"embedded","worker_id":"worker-1","console_href":"/w/workspace-a/workers/worker-1","worker":{"runtime_id":"embedded","worker_id":"worker-1","host_id":"embedded","display_name":"Intake","label":"worker-1","profile":"builtin:intake","singleton_key":null,"tags":[],"workspace":{"visibility":"workspace","identity":"workspace-a","workspace_id":"workspace-a"},"state":"idle","last_seen_at":null,"pinned":false,"retention_state":"active","implementation":{"kind":"runtime","display_hint":"Runtime Worker"},"diagnostics":[]},"diagnostics":[]}"#,
),
]);
let client = BackendWorkspaceProductClient::new_with_access_token(
@@ -1057,7 +1057,7 @@ mod tests {
#[test]
fn workspace_orchestrator_launch_uses_scoped_backend_route() {
let body = r#"{"workspace_id":"workspace-a","online":true,"disposition":"created","worker":{"runtime_id":"embedded","worker_id":"worker-2","host_id":"embedded","display_name":"Orchestrator","label":"worker-2","profile":"builtin:orchestrator","singleton_key":"workspace-orchestrator","tags":[],"workspace":{"visibility":"workspace","identity":"workspace-a","workspace_id":"workspace-a"},"state":"idle","last_seen_at":null,"pinned":true,"retention_state":"active","implementation":{"kind":"runtime","display_hint":"Runtime Worker"},"capabilities":{"can_stop":true,"can_spawn_followup":false},"diagnostics":[]},"diagnostics":[]}"#;
let body = r#"{"workspace_id":"workspace-a","online":true,"disposition":"created","worker":{"runtime_id":"embedded","worker_id":"worker-2","host_id":"embedded","display_name":"Orchestrator","label":"worker-2","profile":"builtin:orchestrator","singleton_key":"workspace-orchestrator","tags":[],"workspace":{"visibility":"workspace","identity":"workspace-a","workspace_id":"workspace-a"},"state":"idle","last_seen_at":null,"pinned":true,"retention_state":"active","implementation":{"kind":"runtime","display_hint":"Runtime Worker"},"diagnostics":[]},"diagnostics":[]}"#;
let (base_url, request, handle) = one_response_server("200 OK", body);
let client = BackendWorkspaceProductClient::new_with_access_token(
base_url,
-16
View File
@@ -6892,7 +6892,6 @@ pub struct RuntimeWorkerSummary {
#[serde(default)]
pub retention_state: String,
pub implementation: WorkerImplementationSummary,
pub capabilities: WorkerCapabilitySummary,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub workdir_attachments: Vec<serde_json::Value>,
#[serde(default)]
@@ -7888,13 +7887,6 @@ pub struct WorkerImplementationSummary {
pub display_hint: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
pub struct WorkerCapabilitySummary {
pub can_stop: bool,
pub can_spawn_followup: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
@@ -8120,7 +8112,6 @@ pub struct WorkerSummary {
#[serde(default)]
pub retention_state: String,
pub implementation: WorkerImplementationSummary,
pub capabilities: WorkerCapabilitySummary,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub workdir_attachments: Vec<WorkerWorkdirAttachmentSummary>,
#[serde(default)]
@@ -8163,7 +8154,6 @@ pub struct WorkerLaunchWorkerSummary {
pub pinned: bool,
pub retention_state: String,
pub implementation: WorkerImplementationSummary,
pub capabilities: WorkerCapabilitySummary,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub workdir_attachments: Vec<RuntimeWorkerWorkdirAttachmentSummary>,
#[serde(default)]
@@ -9339,7 +9329,6 @@ pub fn worker_launch_api_typescript() -> String {
WorkingDirectorySummary::decl(&config),
WorkerWorkspaceSummary::decl(&config),
WorkerImplementationSummary::decl(&config),
WorkerCapabilitySummary::decl(&config),
RuntimeWorkerWorkdirAttachmentSummary::decl(&config),
WorkerLaunchWorkerSummary::decl(&config),
WorkerLaunchRuntimeOption::decl(&config),
@@ -10308,10 +10297,6 @@ mod tests {
kind: "runtime".to_string(),
display_hint: "Runtime Worker".to_string(),
},
capabilities: WorkerCapabilitySummary {
can_stop: true,
can_spawn_followup: false,
},
workdir_attachments: Vec::new(),
diagnostics: Vec::new(),
}
@@ -10791,7 +10776,6 @@ mod tests {
},
"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());
+2 -7
View File
@@ -552,9 +552,8 @@ fn working_directory_text(worker: &BackendWorkerSummary) -> String {
mod tests {
use super::*;
use client::{
BackendDiagnostic, BackendDiagnosticSeverity, BackendWorkerCapabilitySummary,
BackendWorkerImplementationSummary, BackendWorkerRestoreResult,
BackendWorkerWorkspaceSummary,
BackendDiagnostic, BackendDiagnosticSeverity, BackendWorkerImplementationSummary,
BackendWorkerRestoreResult, BackendWorkerWorkspaceSummary,
};
fn worker(runtime_id: &str, worker_id: &str, profile: Option<&str>) -> BackendWorkerSummary {
@@ -588,10 +587,6 @@ mod tests {
kind: "embedded".to_string(),
display_hint: "embedded".to_string(),
},
capabilities: BackendWorkerCapabilitySummary {
can_stop: true,
can_spawn_followup: false,
},
workdir_attachments: Vec::new(),
diagnostics: Vec::new(),
}
+3 -1
View File
@@ -1298,7 +1298,9 @@ mod tests {
#[test]
fn runtime_owned_workspace_client_has_no_fixed_request_timeout() {
let (base_url, server) = delayed_workspace_response(Duration::from_millis(75));
// This must exceed reqwest::blocking::Client's default 30-second timeout.
// A short delay only checks our Option field, not the effective HTTP deadline.
let (base_url, server) = delayed_workspace_response(Duration::from_secs(31));
let client =
RuntimeOwnedWorkspaceClient::new("workspace-a", base_url, "runtime-a", "worker-a");
assert_eq!(client.request_timeout, None);
+26 -55
View File
@@ -240,12 +240,6 @@ pub struct InternalWorkerImplementationSummary {
pub display_hint: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct InternalWorkerCapabilitySummary {
pub can_stop: bool,
pub can_spawn_followup: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct InternalWorkerSummary {
#[serde(flatten)]
@@ -273,7 +267,6 @@ pub struct InternalWorkerSummary {
#[serde(default)]
pub retention_state: String,
pub implementation: InternalWorkerImplementationSummary,
pub capabilities: InternalWorkerCapabilitySummary,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub workdir_attachments: Vec<WorkingDirectoryAttachmentStatus>,
pub diagnostics: Vec<RuntimeDiagnostic>,
@@ -384,10 +377,6 @@ pub(crate) fn workspace_worker_summary(
kind: summary.implementation.kind,
display_hint: summary.implementation.display_hint,
},
capabilities: server_api::WorkerCapabilitySummary {
can_stop: summary.capabilities.can_stop,
can_spawn_followup: summary.capabilities.can_spawn_followup,
},
workdir_attachments,
diagnostics: summary.diagnostics.into_iter().map(Into::into).collect(),
}
@@ -2231,10 +2220,6 @@ impl EmbeddedWorkerRuntime {
Some(EmbeddedWorkerRef::new(EmbeddedWorkerId::parse(worker_id)?))
}
fn can_stop_embedded_worker(&self, status: EmbeddedWorkerStatus) -> bool {
runtime_worker_can_stop(self.execution_enabled, status)
}
fn map_worker_summary(
&self,
summary: worker_runtime::catalog::WorkerSummary,
@@ -2274,11 +2259,6 @@ impl EmbeddedWorkerRuntime {
kind: "embedded_worker_runtime".to_string(),
display_hint: "backend-internal worker-runtime Worker".to_string(),
},
capabilities: InternalWorkerCapabilitySummary {
can_stop: summary.execution_metadata_available
&& self.can_stop_embedded_worker(summary.status),
can_spawn_followup: false,
},
workdir_attachments: summary.workdir_attachments,
diagnostics: embedded_worker_projection_diagnostics(
summary.execution_metadata_available,
@@ -2319,11 +2299,6 @@ impl EmbeddedWorkerRuntime {
kind: "embedded_worker_runtime".to_string(),
display_hint: "backend-internal worker-runtime Worker".to_string(),
},
capabilities: InternalWorkerCapabilitySummary {
can_stop: detail.execution_metadata_available
&& self.can_stop_embedded_worker(detail.status),
can_spawn_followup: false,
},
workdir_attachments: detail.workdir_attachments,
diagnostics: embedded_worker_projection_diagnostics(
detail.execution_metadata_available,
@@ -4127,11 +4102,6 @@ impl RemoteWorkerRuntime {
kind: "remote_worker_runtime".to_string(),
display_hint: "Backend-proxied remote worker-runtime Worker".to_string(),
},
capabilities: InternalWorkerCapabilitySummary {
can_stop: summary.execution_metadata_available
&& runtime_worker_can_stop(true, summary.status),
can_spawn_followup: false,
},
workdir_attachments: summary.workdir_attachments,
diagnostics: remote_worker_projection_diagnostics(summary.execution_metadata_available),
}
@@ -4170,11 +4140,6 @@ impl RemoteWorkerRuntime {
kind: "remote_worker_runtime".to_string(),
display_hint: "Backend-proxied remote worker-runtime Worker".to_string(),
},
capabilities: InternalWorkerCapabilitySummary {
can_stop: detail.execution_metadata_available
&& runtime_worker_can_stop(true, detail.status),
can_spawn_followup: false,
},
workdir_attachments: detail.workdir_attachments,
diagnostics: remote_worker_projection_diagnostics(detail.execution_metadata_available),
}
@@ -5152,10 +5117,6 @@ fn embedded_runtime_status_label(status: RuntimeStatus) -> &'static str {
}
}
fn runtime_worker_can_stop(execution_enabled: bool, status: EmbeddedWorkerStatus) -> bool {
execution_enabled && status.is_active()
}
fn embedded_worker_status_label(status: EmbeddedWorkerStatus) -> &'static str {
match status {
EmbeddedWorkerStatus::Idle => "idle",
@@ -5963,10 +5924,6 @@ pub fn placeholder_worker(host_id: impl Into<String>) -> InternalWorkerSummary {
kind: "placeholder".to_string(),
display_hint: "unsupported".to_string(),
},
capabilities: InternalWorkerCapabilitySummary {
can_stop: false,
can_spawn_followup: false,
},
workdir_attachments: Vec::new(),
diagnostics: vec![diagnostic(
"runtime_capability_unsupported",
@@ -6621,10 +6578,6 @@ mod tests {
kind: "fixture".to_string(),
display_hint: "test fixture".to_string(),
},
capabilities: InternalWorkerCapabilitySummary {
can_stop: false,
can_spawn_followup: false,
},
workdir_attachments: Vec::new(),
diagnostics: Vec::new(),
}],
@@ -7123,7 +7076,12 @@ mod tests {
let spawned = runtime.spawn_worker(test_create_binding(), embedded_spawn_request());
assert_eq!(spawned.state, InternalWorkerOperationState::Accepted);
let worker = spawned.worker.expect("created embedded worker");
assert!(worker.capabilities.can_stop);
assert!(
serde_json::to_value(&worker)
.unwrap()
.get("capabilities")
.is_none()
);
let input = runtime.send_input(
&worker.worker.worker_id,
@@ -7496,7 +7454,12 @@ mod tests {
workers.items[0].workspace.identity,
"runtime_registry_worker"
);
assert!(workers.items[0].capabilities.can_stop);
assert!(
serde_json::to_value(&workers.items[0])
.unwrap()
.get("capabilities")
.is_none()
);
let input = registry
.send_input(
@@ -7581,11 +7544,14 @@ mod tests {
let workers = registry.list_workers(10);
assert_eq!(workers.items.len(), 5);
assert!(!workers.items[0].capabilities.can_stop);
assert!(workers.items[1].capabilities.can_stop);
assert!(workers.items[2].capabilities.can_stop);
assert!(workers.items[3].capabilities.can_stop);
assert!(!workers.items[4].capabilities.can_stop);
for worker in &workers.items {
assert!(
serde_json::to_value(worker)
.unwrap()
.get("capabilities")
.is_none()
);
}
assert_eq!(workers.items[0].state, "stopped");
assert_eq!(workers.items[1].state, "running");
assert_eq!(workers.items[2].state, "paused");
@@ -7601,7 +7567,12 @@ mod tests {
let stopped_detail = registry
.worker(&RuntimeWorkerRef::new("remote:primary", &worker_id))
.unwrap();
assert!(!stopped_detail.capabilities.can_stop);
assert!(
serde_json::to_value(&stopped_detail)
.unwrap()
.get("capabilities")
.is_none()
);
assert_eq!(stopped_detail.state, "stopped");
server.join().expect("mock remote server finished");
+22 -39
View File
@@ -152,17 +152,16 @@ use crate::companion::{
use crate::config_source::ConfigCommitRequest;
use crate::hosts::{
EMBEDDED_RUNTIME_ID, EmbeddedWorkerRuntime, HostDiagnosticSeverity, InternalHostSummary,
InternalWorkerCapabilitySummary, InternalWorkerImplementationSummary,
InternalWorkerOperationState, InternalWorkerRestoreResult, InternalWorkerSummary,
InternalWorkerWorkspaceSummary, RemoteRuntimeConfig, RemoteWorkerRuntime, RuntimeDiagnostic,
RuntimePingFailureKind, RuntimeRegistry, RuntimeRegistryError, RuntimeRegistryUnregisterResult,
TicketWorkerRole, WorkerCompletionsRequest, WorkerCompletionsResult, WorkerControlOperation,
WorkerCreateBinding, WorkerInputKind, WorkerInputRequest, WorkerInputResult,
WorkerLifecycleRequest, WorkerLifecycleResult, WorkerSpawnAcceptanceRequirement,
WorkerSpawnIntent, WorkerSpawnRequest, WorkerSpawnResult, WorkerSpawnWorkingDirectoryRequest,
WorkerTicketAssignmentRequest, WorkspaceRuntimeAuthorization,
is_disallowed_remote_runtime_address, is_loopback_runtime_origin,
worker_spawn_create_fingerprint, workspace_worker_summary,
InternalWorkerImplementationSummary, InternalWorkerOperationState, InternalWorkerRestoreResult,
InternalWorkerSummary, InternalWorkerWorkspaceSummary, RemoteRuntimeConfig,
RemoteWorkerRuntime, RuntimeDiagnostic, RuntimePingFailureKind, RuntimeRegistry,
RuntimeRegistryError, RuntimeRegistryUnregisterResult, TicketWorkerRole,
WorkerCompletionsRequest, WorkerCompletionsResult, WorkerControlOperation, WorkerCreateBinding,
WorkerInputKind, WorkerInputRequest, WorkerInputResult, WorkerLifecycleRequest,
WorkerLifecycleResult, WorkerSpawnAcceptanceRequirement, WorkerSpawnIntent, WorkerSpawnRequest,
WorkerSpawnResult, WorkerSpawnWorkingDirectoryRequest, WorkerTicketAssignmentRequest,
WorkspaceRuntimeAuthorization, is_disallowed_remote_runtime_address,
is_loopback_runtime_origin, worker_spawn_create_fingerprint, workspace_worker_summary,
};
use crate::memory_backend::execute_memory_backend_operation_with_authority;
use crate::memory_staging::{
@@ -5175,10 +5174,6 @@ fn runtime_worker_summary_to_api(
kind: worker.implementation.kind,
display_hint: worker.implementation.display_hint,
},
capabilities: server_api::WorkerCapabilitySummary {
can_stop: worker.capabilities.can_stop,
can_spawn_followup: worker.capabilities.can_spawn_followup,
},
workdir_attachments: worker
.workdir_attachments
.into_iter()
@@ -16956,10 +16951,6 @@ fn worker_launch_worker_summary(worker: InternalWorkerSummary) -> WorkerLaunchWo
kind: worker.implementation.kind,
display_hint: worker.implementation.display_hint,
},
capabilities: server_api::WorkerCapabilitySummary {
can_stop: worker.capabilities.can_stop,
can_spawn_followup: worker.capabilities.can_spawn_followup,
},
workdir_attachments: worker
.workdir_attachments
.into_iter()
@@ -24806,10 +24797,6 @@ fn worker_summary_from_registry(record: &WorkerRegistryRecord) -> InternalWorker
last_seen_at: Some(record.updated_at.clone()),
pinned: record.retention_state == "pinned",
retention_state: record.retention_state.clone(),
capabilities: InternalWorkerCapabilitySummary {
can_stop: false,
can_spawn_followup: false,
},
workspace: InternalWorkerWorkspaceSummary {
visibility: "backend_registry".to_string(),
identity: record.workspace_id.clone(),
@@ -24855,8 +24842,6 @@ fn worker_summary_from_projection(
.then(|| observation.worker.worker_state.clone())
.flatten();
summary.last_seen_at = Some(observation.observed_at.clone());
summary.capabilities.can_stop = observed
&& observation.worker.state != protocol::subscription::SubscriptionWorkerState::Stopped;
summary.implementation.display_hint = if observed {
"Runtime-backed Worker".to_string()
} else {
@@ -26709,7 +26694,12 @@ mod tests {
assert_eq!(unavailable.availability, availability);
assert_eq!(unavailable.state, "unavailable");
assert_eq!(unavailable.worker_state, None);
assert!(!unavailable.capabilities.can_stop);
assert!(
serde_json::to_value(&unavailable)
.unwrap()
.get("capabilities")
.is_none()
);
let mut observed_stopped = projection;
let observation = observed_stopped.observation.as_mut().unwrap();
@@ -28645,10 +28635,6 @@ mod tests {
kind: "fixture".to_string(),
display_hint: "Workdirless fixture".to_string(),
},
capabilities: InternalWorkerCapabilitySummary {
can_stop: true,
can_spawn_followup: false,
},
workdir_attachments: Vec::new(),
diagnostics: Vec::new(),
}
@@ -40983,10 +40969,6 @@ mod tests {
kind: "remote".to_string(),
display_hint: "remote".to_string(),
},
capabilities: server_api::WorkerCapabilitySummary {
can_stop: true,
can_spawn_followup: false,
},
workdir_attachments: Vec::new(),
diagnostics: Vec::new(),
}
@@ -41081,10 +41063,6 @@ mod tests {
kind: "remote".to_string(),
display_hint: "remote".to_string(),
},
capabilities: server_api::WorkerCapabilitySummary {
can_stop: true,
can_spawn_followup: false,
},
workdir_attachments: Vec::new(),
diagnostics: Vec::new(),
}],
@@ -43445,7 +43423,12 @@ mod tests {
// catalog remains controllable while its failed execution is represented in
// `worker_state` rather than by removing the catalog Worker.
assert_eq!(restored_worker.state, "idle");
assert!(restored_worker.capabilities.can_stop);
assert!(
serde_json::to_value(&restored_worker)
.unwrap()
.get("capabilities")
.is_none()
);
let bundles = restored
.runtime
+4 -35
View File
@@ -7448,9 +7448,6 @@
"$ref": "#/components/schemas/SubscriptionWorkerAvailability",
"description": "Freshness of the Runtime observation used to construct `worker_state`.\n`Unavailable` means no current controller snapshot is being asserted."
},
"capabilities": {
"$ref": "#/components/schemas/WorkerCapabilitySummary"
},
"diagnostics": {
"default": [],
"items": {
@@ -7550,7 +7547,6 @@
"availability",
"state",
"implementation",
"capabilities",
"updated_at"
],
"type": "object"
@@ -7791,9 +7787,6 @@
"RuntimeWorkerSummary": {
"additionalProperties": false,
"properties": {
"capabilities": {
"$ref": "#/components/schemas/WorkerCapabilitySummary"
},
"diagnostics": {
"default": [],
"items": {
@@ -7872,8 +7865,7 @@
"label",
"workspace",
"state",
"implementation",
"capabilities"
"implementation"
],
"type": "object"
},
@@ -11254,21 +11246,6 @@
}
]
},
"WorkerCapabilitySummary": {
"properties": {
"can_spawn_followup": {
"type": "boolean"
},
"can_stop": {
"type": "boolean"
}
},
"required": [
"can_stop",
"can_spawn_followup"
],
"type": "object"
},
"WorkerFileDeleteResponse": {
"additionalProperties": false,
"properties": {
@@ -11447,9 +11424,6 @@
"additionalProperties": false,
"description": "Runtime-owned Worker summary embedded in Worker launch responses.\n\nThis preserves the existing launch wire shape. Workspace-owned Worker list\nand detail responses use [`WorkerSummary`] instead.",
"properties": {
"capabilities": {
"$ref": "#/components/schemas/WorkerCapabilitySummary"
},
"diagnostics": {
"default": [],
"items": {
@@ -11529,8 +11503,7 @@
"state",
"pinned",
"retention_state",
"implementation",
"capabilities"
"implementation"
],
"type": "object"
},
@@ -11830,9 +11803,6 @@
"$ref": "#/components/schemas/SubscriptionWorkerAvailability",
"description": "Freshness of the Runtime observation used to construct `worker_state`.\n`Unavailable` means no current controller snapshot is being asserted."
},
"capabilities": {
"$ref": "#/components/schemas/WorkerCapabilitySummary"
},
"diagnostics": {
"default": [],
"items": {
@@ -11928,8 +11898,7 @@
"workspace",
"availability",
"state",
"implementation",
"capabilities"
"implementation"
],
"type": "object"
},
@@ -14020,7 +13989,7 @@
"info": {
"title": "Yoi Server API",
"version": "0.1.0",
"x-yoi-source-digest": "sha256:49856606e4a16d138cc04a62a6096fd40fff213587b1cbf7f0594f28994a45b0"
"x-yoi-source-digest": "sha256:ae559492220ec118f04c2c0c9593e8b3cb44c3cb857be3ae85715ace26da8043"
},
"openapi": "3.1.0",
"paths": {
@@ -31,7 +31,6 @@ const worker = {
profile: "default",
tags: [],
diagnostics: [],
capabilities: { can_stop: false, can_spawn_followup: false },
workspace: {
workspace_id: workspaceId,
visibility: "workspace",
@@ -1,6 +1,6 @@
// @generated by yoi-companion-openapi-typescript@1. Do not edit.
// Input: openapi/server-api.json
// Input contract: 0.1.0 (sha256:49856606e4a16d138cc04a62a6096fd40fff213587b1cbf7f0594f28994a45b0)
// Input contract: 0.1.0 (sha256:ae559492220ec118f04c2c0c9593e8b3cb44c3cb857be3ae85715ace26da8043)
// Options: companion-operation-schema-closure,openapi-3.1,strict-integer-ranges,sorted-output
// Output: web/workspace/src/lib/generated/companion-api.ts
// Regenerate/check: cargo run -q -p server-api --example generate_companion_api_types -- --check
@@ -1,6 +1,6 @@
// @generated by yoi-repository-openapi-typescript@1. Do not edit.
// Input: openapi/server-api.json
// Input contract: 0.1.0 (sha256:49856606e4a16d138cc04a62a6096fd40fff213587b1cbf7f0594f28994a45b0)
// Input contract: 0.1.0 (sha256:ae559492220ec118f04c2c0c9593e8b3cb44c3cb857be3ae85715ace26da8043)
// Options: repository-operation-schema-closure,openapi-3.1,format:uint64=safe-number,strict-safe-integers,sorted-output
// Output: web/workspace/src/lib/generated/repository-api.ts
// Regenerate/check: cargo run -q -p server-api --example generate_repository_openapi_types -- --check
@@ -61,8 +61,6 @@ export type SubscriptionWorkerAvailability = "observed" | "unavailable";
export type WorkerBusyState = { kind: "run"; state: WorkerRunState; } | { kind: "maintenance"; state: WorkerMaintenanceState; };
export type WorkerCapabilitySummary = { can_spawn_followup: boolean; can_stop: boolean; };
export type WorkerImplementationSummary = { display_hint: string; kind: string; };
export type WorkerMaintenanceState = "compacting";
@@ -73,7 +71,7 @@ export type WorkerState = { kind: "idle"; } | { kind: "busy"; state: WorkerBusyS
export type WorkerStateSnapshot = { last_command_id: number; state: WorkerState; };
export type WorkerSummary = { availability: SubscriptionWorkerAvailability; capabilities: WorkerCapabilitySummary; diagnostics?: Array<Diagnostic>; display_name?: string; host_id: string; implementation: WorkerImplementationSummary; label: string; last_seen_at?: string | null; pinned?: boolean; profile?: string | null; resource_key: string; retention_state?: string; runtime_id: string; singleton_key?: string | null; state: string; tags?: Array<string>; workdir_attachments?: Array<WorkerWorkdirAttachmentSummary>; worker_id: string; worker_state?: WorkerStateSnapshot | null; workspace: WorkerWorkspaceSummary; };
export type WorkerSummary = { availability: SubscriptionWorkerAvailability; diagnostics?: Array<Diagnostic>; display_name?: string; host_id: string; implementation: WorkerImplementationSummary; label: string; last_seen_at?: string | null; pinned?: boolean; profile?: string | null; resource_key: string; retention_state?: string; runtime_id: string; singleton_key?: string | null; state: string; tags?: Array<string>; workdir_attachments?: Array<WorkerWorkdirAttachmentSummary>; worker_id: string; worker_state?: WorkerStateSnapshot | null; workspace: WorkerWorkspaceSummary; };
export type WorkerWorkdirAttachmentSummary = { alias: string; effective_permissions: ExternalWorkdirPermissions; working_directory: WorkingDirectorySummary; };
@@ -1,6 +1,6 @@
// @generated by yoi-runtime-openapi-typescript@1. Do not edit.
// Input: openapi/server-api.json
// Input contract: 0.1.0 (sha256:49856606e4a16d138cc04a62a6096fd40fff213587b1cbf7f0594f28994a45b0)
// Input contract: 0.1.0 (sha256:ae559492220ec118f04c2c0c9593e8b3cb44c3cb857be3ae85715ace26da8043)
// Options: runtime-operation-schema-closure,openapi-3.1,strict-integer-ranges,sorted-output
// Output: web/workspace/src/lib/generated/runtime-api.ts
// Regenerate/check: cargo run -q -p server-api --example generate_runtime_api_types -- --check
@@ -107,7 +107,7 @@ export type RuntimeWorkerLifecycleResult = { diagnostics?: Array<Diagnostic>; ru
export type RuntimeWorkerListResponse = { diagnostics?: Array<Diagnostic>; items: Array<WorkerSummary>; limit: number; source: string; workspace_id: string; };
export type RuntimeWorkerShowResponse = { availability: SubscriptionWorkerAvailability; capabilities: WorkerCapabilitySummary; diagnostics?: Array<Diagnostic>; display_name?: string; host_id: string; implementation: WorkerImplementationSummary; label: string; last_seen_at?: string | null; pinned?: boolean; profile?: string | null; resource_key: string; retention_state?: string; runtime_id: string; singleton_key?: string | null; state: string; tags?: Array<string>; updated_at: string; workdir_attachments?: Array<WorkerWorkdirAttachmentSummary>; worker_id: string; worker_state?: WorkerStateSnapshot | null; workspace: WorkerWorkspaceSummary; };
export type RuntimeWorkerShowResponse = { availability: SubscriptionWorkerAvailability; diagnostics?: Array<Diagnostic>; display_name?: string; host_id: string; implementation: WorkerImplementationSummary; label: string; last_seen_at?: string | null; pinned?: boolean; profile?: string | null; resource_key: string; retention_state?: string; runtime_id: string; singleton_key?: string | null; state: string; tags?: Array<string>; updated_at: string; workdir_attachments?: Array<WorkerWorkdirAttachmentSummary>; worker_id: string; worker_state?: WorkerStateSnapshot | null; workspace: WorkerWorkspaceSummary; };
export type RuntimeWorkerSpawnAcceptanceEvidence = { detail: string; kind: string; };
@@ -123,7 +123,7 @@ export type RuntimeWorkerSpawnWorkingDirectoryAttachmentRequest = { alias: strin
export type RuntimeWorkerSpawnWorkingDirectoryRequest = { repository_key: string; selector?: string | null; };
export type RuntimeWorkerSummary = { capabilities: WorkerCapabilitySummary; diagnostics?: Array<Diagnostic>; display_name?: string; host_id: string; implementation: WorkerImplementationSummary; label: string; last_seen_at?: string | null; pinned?: boolean; profile?: string | null; retention_state?: string; runtime_id: string; singleton_key?: string | null; state: string; tags?: Array<string>; workdir_attachments?: Array<unknown>; worker_id: string; worker_state?: unknown; workspace: WorkerWorkspaceSummary; };
export type RuntimeWorkerSummary = { diagnostics?: Array<Diagnostic>; display_name?: string; host_id: string; implementation: WorkerImplementationSummary; label: string; last_seen_at?: string | null; pinned?: boolean; profile?: string | null; retention_state?: string; runtime_id: string; singleton_key?: string | null; state: string; tags?: Array<string>; workdir_attachments?: Array<unknown>; worker_id: string; worker_state?: unknown; workspace: WorkerWorkspaceSummary; };
export type RuntimeWorkerTicketAssignmentRequest = { operation_id: string; ticket_id: string; };
@@ -137,8 +137,6 @@ export type UploadedFileRef = { artifact_id: string; availability: UploadedFileA
export type WorkerBusyState = { kind: "run"; state: WorkerRunState; } | { kind: "maintenance"; state: WorkerMaintenanceState; };
export type WorkerCapabilitySummary = { can_spawn_followup: boolean; can_stop: boolean; };
export type WorkerFileDeleteResponse = { deleted: boolean; };
export type WorkerFileUploadResponse = { file: UploadedFileRef; };
@@ -163,7 +161,7 @@ export type WorkerState = { kind: "idle"; } | { kind: "busy"; state: WorkerBusyS
export type WorkerStateSnapshot = { last_command_id: number; state: WorkerState; };
export type WorkerSummary = { availability: SubscriptionWorkerAvailability; capabilities: WorkerCapabilitySummary; diagnostics?: Array<Diagnostic>; display_name?: string; host_id: string; implementation: WorkerImplementationSummary; label: string; last_seen_at?: string | null; pinned?: boolean; profile?: string | null; resource_key: string; retention_state?: string; runtime_id: string; singleton_key?: string | null; state: string; tags?: Array<string>; workdir_attachments?: Array<WorkerWorkdirAttachmentSummary>; worker_id: string; worker_state?: WorkerStateSnapshot | null; workspace: WorkerWorkspaceSummary; };
export type WorkerSummary = { availability: SubscriptionWorkerAvailability; diagnostics?: Array<Diagnostic>; display_name?: string; host_id: string; implementation: WorkerImplementationSummary; label: string; last_seen_at?: string | null; pinned?: boolean; profile?: string | null; resource_key: string; retention_state?: string; runtime_id: string; singleton_key?: string | null; state: string; tags?: Array<string>; workdir_attachments?: Array<WorkerWorkdirAttachmentSummary>; worker_id: string; worker_state?: WorkerStateSnapshot | null; workspace: WorkerWorkspaceSummary; };
export type WorkerWorkdirAttachmentSummary = { alias: string; effective_permissions: ExternalWorkdirPermissions; working_directory: WorkingDirectorySummary; };
@@ -1,6 +1,6 @@
// @generated by yoi-ticket-browser-openapi-typescript@1. Do not edit.
// Input: openapi/server-api.json
// Input contract: 0.1.0 (sha256:49856606e4a16d138cc04a62a6096fd40fff213587b1cbf7f0594f28994a45b0)
// Input contract: 0.1.0 (sha256:ae559492220ec118f04c2c0c9593e8b3cb44c3cb857be3ae85715ace26da8043)
// Options: ticket-objective-merge-request-browser-operation-schema-closure,openapi-3.1,strict-integer-ranges,sorted-output
// Output: web/workspace/src/lib/generated/ticket-api.ts
// Regenerate/check: cargo run -q -p server-api --example generate_ticket_api_types -- --check
@@ -1,6 +1,6 @@
// @generated by yoi-worker-openapi-typescript@1. Do not edit.
// Input: openapi/server-api.json
// Input contract: 0.1.0 (sha256:49856606e4a16d138cc04a62a6096fd40fff213587b1cbf7f0594f28994a45b0)
// Input contract: 0.1.0 (sha256:ae559492220ec118f04c2c0c9593e8b3cb44c3cb857be3ae85715ace26da8043)
// Options: worker-operation-schema-closure,openapi-3.1,strict-integer-ranges,sorted-output
// Output: web/workspace/src/lib/generated/worker-launch-api.ts
// Regenerate/check: cargo run -q -p server-api --example generate_worker_launch_api_types -- --check
@@ -45,8 +45,6 @@ export type UploadedFileRef = { artifact_id: string; availability: UploadedFileA
export type WorkerBusyState = { kind: "run"; state: WorkerRunState; } | { kind: "maintenance"; state: WorkerMaintenanceState; };
export type WorkerCapabilitySummary = { can_spawn_followup: boolean; can_stop: boolean; };
export type WorkerImplementationSummary = { display_hint: string; kind: string; };
export type WorkerLaunchOptionsResponse = { default_profile?: string | null; diagnostics: Array<Diagnostic>; profiles: Array<WorkerLaunchProfileCandidate>; repositories: Array<WorkingDirectoryRepositoryOption>; runtimes: Array<WorkerLaunchRuntimeOption>; working_directories: Array<WorkingDirectorySummary>; workspace_id: string; };
@@ -55,7 +53,7 @@ export type WorkerLaunchProfileCandidate = { description: string; id: string; la
export type WorkerLaunchRuntimeOption = { built_in: boolean; diagnostics: Array<Diagnostic>; display_name: string; runtime_id: string; status: string; worker_creation_available: boolean; working_directory_required: boolean; };
export type WorkerLaunchWorkerSummary = { capabilities: WorkerCapabilitySummary; diagnostics?: Array<Diagnostic>; display_name: string; host_id: string; implementation: WorkerImplementationSummary; label: string; last_seen_at?: string | null; pinned: boolean; profile?: string | null; retention_state: string; runtime_id: string; singleton_key?: string | null; state: string; tags: Array<string>; workdir_attachments?: Array<RuntimeWorkerWorkdirAttachmentSummary>; worker_id: string; workspace: WorkerWorkspaceSummary; };
export type WorkerLaunchWorkerSummary = { diagnostics?: Array<Diagnostic>; display_name: string; host_id: string; implementation: WorkerImplementationSummary; label: string; last_seen_at?: string | null; pinned: boolean; profile?: string | null; retention_state: string; runtime_id: string; singleton_key?: string | null; state: string; tags: Array<string>; workdir_attachments?: Array<RuntimeWorkerWorkdirAttachmentSummary>; worker_id: string; workspace: WorkerWorkspaceSummary; };
export type WorkerListResponse = { diagnostics?: Array<Diagnostic>; items: Array<WorkerSummary>; limit: number; source: string; workspace_id: string; };
@@ -67,7 +65,7 @@ export type WorkerState = { kind: "idle"; } | { kind: "busy"; state: WorkerBusyS
export type WorkerStateSnapshot = { last_command_id: number; state: WorkerState; };
export type WorkerSummary = { availability: SubscriptionWorkerAvailability; capabilities: WorkerCapabilitySummary; diagnostics?: Array<Diagnostic>; display_name?: string; host_id: string; implementation: WorkerImplementationSummary; label: string; last_seen_at?: string | null; pinned?: boolean; profile?: string | null; resource_key: string; retention_state?: string; runtime_id: string; singleton_key?: string | null; state: string; tags?: Array<string>; workdir_attachments?: Array<WorkerWorkdirAttachmentSummary>; worker_id: string; worker_state?: WorkerStateSnapshot | null; workspace: WorkerWorkspaceSummary; };
export type WorkerSummary = { availability: SubscriptionWorkerAvailability; diagnostics?: Array<Diagnostic>; display_name?: string; host_id: string; implementation: WorkerImplementationSummary; label: string; last_seen_at?: string | null; pinned?: boolean; profile?: string | null; resource_key: string; retention_state?: string; runtime_id: string; singleton_key?: string | null; state: string; tags?: Array<string>; workdir_attachments?: Array<WorkerWorkdirAttachmentSummary>; worker_id: string; worker_state?: WorkerStateSnapshot | null; workspace: WorkerWorkspaceSummary; };
export type WorkerWorkdirAttachmentSummary = { alias: string; effective_permissions: ExternalWorkdirPermissions; working_directory: WorkingDirectorySummary; };
@@ -56,13 +56,28 @@ const worker = {
kind: "runtime",
display_hint: "Runtime Worker",
},
capabilities: {
can_stop: true,
can_spawn_followup: false,
},
diagnostics: [],
};
Deno.test("Worker summaries omit the removed management capabilities field", () => {
const summary = { ...worker, resource_key: "W-1", availability: "observed" };
assertEquals("capabilities" in parseWorkerSummary(summary), false);
assertThrows(
() => parseWorkerSummary({ ...summary, capabilities: {} }),
Error,
"unknown field capabilities",
);
const created = parseBrowserCreateWorkerResponse({
workspace_id: "workspace-a",
runtime_id: "runtime-a",
worker_id: "worker-a",
console_href: "/w/workspace-a/workers/W-1/console",
worker,
diagnostics: [],
});
assertEquals("capabilities" in created.worker, false);
});
Deno.test("Worker summary parser enforces observation freshness", () => {
const unavailable = {
...worker,
@@ -10,7 +10,6 @@ import type {
RuntimeWorkerWorkdirAttachmentSummary,
RuntimeWorkingDirectoryCleanupTarget,
RuntimeWorkingDirectorySummary,
WorkerCapabilitySummary,
WorkerImplementationSummary,
WorkerLaunchOptionsResponse,
WorkerLaunchProfileCandidate,
@@ -251,21 +250,6 @@ function implementationSummary(
};
}
function capabilitySummary(
value: unknown,
label: string,
): WorkerCapabilitySummary {
const item = record(value, label);
exact(item, ["can_stop", "can_spawn_followup"], label);
return {
can_stop: boolean(item.can_stop, `${label}.can_stop`),
can_spawn_followup: boolean(
item.can_spawn_followup,
`${label}.can_spawn_followup`,
),
};
}
function workerStateSnapshot(
value: unknown,
label: string,
@@ -374,7 +358,6 @@ export function parseWorkerSummary(
"pinned",
"retention_state",
"implementation",
"capabilities",
"workdir_attachments",
"diagnostics",
],
@@ -423,7 +406,6 @@ export function parseWorkerSummary(
item.implementation,
`${label}.implementation`,
),
capabilities: capabilitySummary(item.capabilities, `${label}.capabilities`),
workdir_attachments: item.workdir_attachments === undefined
? undefined
: array(
@@ -617,7 +599,6 @@ function workerSummary(
"pinned",
"retention_state",
"implementation",
"capabilities",
"workdir_attachments",
"diagnostics",
],
@@ -641,7 +622,6 @@ function workerSummary(
item.implementation,
`${label}.implementation`,
),
capabilities: capabilitySummary(item.capabilities, `${label}.capabilities`),
workdir_attachments: item.workdir_attachments === undefined
? undefined
: array(
@@ -3,6 +3,7 @@
import { workerConsoleHref } from '$lib/workspace/resource-links';
import { pushWorkspaceAlert } from '$lib/workspace/alerts/store';
import {
canStopSidebarWorker,
canDeleteSidebarWorker,
deleteSidebarWorker,
stopSidebarWorker,
@@ -91,14 +92,14 @@
}
async function stopWorker(worker: SidebarWorker) {
if (busyAction || !worker.capabilities.can_stop) return;
if (busyAction || !canStopSidebarWorker(worker)) return;
closeWorkerMenu();
busyAction = { workerKey: workerKey(worker), kind: 'stop' };
try {
await stopSidebarWorker(workspaceId, worker);
workers = workers.map((item) =>
workerKey(item) === workerKey(worker)
? { ...item, state: 'stopped', capabilities: { ...item.capabilities, can_stop: false } }
? { ...item, state: 'stopped', lifecycleState: 'stopped' }
: item
);
pushWorkspaceAlert('info', `${worker.display_name || worker.label} stopped`, {
@@ -226,7 +227,7 @@
<button
type="button"
role="menuitem"
disabled={busyAction !== null || !worker.capabilities.can_stop}
disabled={busyAction !== null || !canStopSidebarWorker(worker)}
onclick={() => stopWorker(worker)}
>
{isBusy(worker, 'stop') ? 'Stopping…' : 'Stop'}
@@ -10,7 +10,6 @@ import type {
BrowserWorkerWorkingDirectorySelection
as SharedBrowserWorkerWorkingDirectorySelection,
Diagnostic as SharedDiagnostic,
WorkerCapabilitySummary as SharedWorkerCapabilitySummary,
WorkerLaunchOptionsResponse as SharedWorkerLaunchOptionsResponse,
WorkerLaunchProfileCandidate as SharedWorkerLaunchProfileCandidate,
WorkerLaunchRuntimeOption as SharedWorkerLaunchRuntimeOption,
@@ -78,8 +77,6 @@ export type Runtime = {
};
};
export type WorkerCapabilities = SharedWorkerCapabilitySummary;
export type WorkerWorkdirAttachment = SharedWorkerWorkdirAttachmentSummary;
export type Worker =
@@ -4,10 +4,7 @@ import {
parseRuntimeCleanupPlan,
parseRuntimeWorkerLifecycleResult,
} from "$lib/workspace/api/runtime-workers";
import type {
Diagnostic,
Worker,
} from "./types";
import type { Diagnostic, Worker } from "./types";
type FetchFn = typeof fetch;
type WorkerActionTarget = Pick<Worker, "runtime_id" | "worker_id" | "state">;
@@ -25,13 +22,17 @@ async function responseError(response: Response): Promise<string> {
const fallback = `${response.status} ${response.statusText}`.trim();
try {
const payload: unknown = await response.json();
if (typeof payload !== "object" || payload === null || Array.isArray(payload)) {
if (
typeof payload !== "object" || payload === null || Array.isArray(payload)
) {
return fallback;
}
const record = payload as Record<string, unknown>;
if (typeof record.message === "string") return record.message.slice(0, 512);
const nested = record.error;
if (typeof nested === "object" && nested !== null && !Array.isArray(nested)) {
if (
typeof nested === "object" && nested !== null && !Array.isArray(nested)
) {
const message = (nested as Record<string, unknown>).message;
if (typeof message === "string") return message.slice(0, 512);
}
@@ -51,6 +52,14 @@ function diagnosticMessage(
fallback;
}
export function canStopSidebarWorker(worker: {
availability: Worker["availability"];
lifecycleState: string;
}): boolean {
return worker.availability === "observed" &&
["idle", "running", "paused"].includes(worker.lifecycleState);
}
export function canDeleteSidebarWorker(worker: WorkerActionTarget): boolean {
return worker.state === "stopped";
}
@@ -112,7 +121,9 @@ export async function deleteSidebarWorker(
throw new Error(await responseError(executionResponse));
}
const execution = parseRuntimeCleanupExecution(await executionResponse.json());
const execution = parseRuntimeCleanupExecution(
await executionResponse.json(),
);
const outcome = execution.results.find((result) =>
result.target_id === candidate.target_id
);
@@ -0,0 +1,136 @@
// @vitest-environment happy-dom
import {
cleanup,
fireEvent,
render,
screen,
waitFor,
} from "@testing-library/svelte";
import { afterEach, expect, test, vi } from "vitest";
import type {
SubscriptionFrame,
SubscriptionWorker,
} from "$lib/generated/protocol";
import WorkersNavSection from "./WorkersNavSection.svelte";
import { disposeWorkspaceWorkersStore } from "./worker-subscription";
const transport = vi.hoisted(() => ({
listener: null as { onFrame(frame: SubscriptionFrame): void } | null,
close: vi.fn(),
}));
vi.mock("$lib/workspace/multiplexer", () => ({
workspaceMultiplexer: () => ({
subscribe: (
_selector: unknown,
listener: NonNullable<typeof transport.listener>,
) => {
transport.listener = listener;
return { close: transport.close };
},
}),
}));
afterEach(() => {
cleanup();
disposeWorkspaceWorkersStore("stop-test");
transport.listener = null;
vi.unstubAllGlobals();
});
function snapshot(
state: SubscriptionWorker["state"],
availability: SubscriptionWorker["availability"],
): SubscriptionFrame {
return {
protocol_version: 1,
frame: "response",
message: {
result: "subscribed",
payload: {
request_id: "request-1",
subscription_id: "subscription-1",
selector: { topic: "workspace_workers" },
snapshot_revision: 1,
snapshot: {
topic: "workers",
data: {
workers: [{
worker_id: "worker-a",
runtime_id: "runtime-a",
resource_key: "W-1",
availability,
subject_revision: 1,
state,
// An absent foreground snapshot must not prevent stopping an active catalog Worker.
worker_state: null,
has_running_internal_workers: false,
display_name: "Test worker",
workspace_id: "stop-test",
workdir_attachments: [],
}],
},
},
},
},
};
}
test.each(["idle", "running", "paused", "stopped"] as const)(
"Stop uses observed catalog state without capabilities (%s)",
async (state) => {
const request = vi.fn(async (input: RequestInfo | URL) =>
String(input).endsWith("/stop")
? Response.json({
state: "accepted",
runtime_id: "runtime-a",
worker_id: "worker-a",
diagnostics: [],
})
: new Response(null, { status: 404 })
);
vi.stubGlobal("fetch", request);
render(WorkersNavSection, { workspaceId: "stop-test" });
await waitFor(() => expect(transport.listener).not.toBeNull());
transport.listener!.onFrame(snapshot(state, "observed"));
await fireEvent.click(
await screen.findByRole("button", { name: "Actions for Test worker" }),
);
const stop = screen.getByRole("menuitem", {
name: "Stop",
}) as HTMLButtonElement;
expect(stop.disabled).toBe(state === "stopped");
if (state === "stopped") return;
await fireEvent.click(stop);
await waitFor(() =>
expect(request.mock.calls.some(([url]) => String(url).endsWith("/stop")))
.toBe(true)
);
await fireEvent.click(
screen.getByRole("button", { name: "Actions for Test worker" }),
);
await waitFor(() =>
expect(
(screen.getByRole("menuitem", { name: "Stop" }) as HTMLButtonElement)
.disabled,
).toBe(true)
);
},
);
test("Unavailable observation disables Stop even when catalog state is running", async () => {
vi.stubGlobal(
"fetch",
vi.fn(async () => new Response(null, { status: 404 })),
);
render(WorkersNavSection, { workspaceId: "stop-test" });
await waitFor(() => expect(transport.listener).not.toBeNull());
transport.listener!.onFrame(snapshot("running", "unavailable"));
await fireEvent.click(
await screen.findByRole("button", { name: "Actions for Test worker" }),
);
expect(
(screen.getByRole("menuitem", { name: "Stop" }) as HTMLButtonElement)
.disabled,
).toBe(true);
});
@@ -16,6 +16,8 @@ import type { Worker } from "./types";
export type SidebarWorker = Omit<Worker, "workdir_attachments"> & {
workdir_attachments: SidebarWorkdirAttachment[];
has_running_internal_workers: boolean;
/** Catalog lifecycle, independent of the possibly unknown foreground display state. */
lifecycleState: SubscriptionWorker["state"];
};
export type WorkspaceWorkersState = {
@@ -148,6 +150,7 @@ function projectWorker(
},
availability: worker.availability,
state: liveWorkerState(worker),
lifecycleState: worker.state,
worker_state: worker.worker_state,
pinned: false,
retention_state: "transient",
@@ -155,11 +158,6 @@ function projectWorker(
kind: "runtime_subscription_worker",
display_hint: "Workspace-authorized Runtime Worker",
},
capabilities: {
can_stop: worker.availability !== "unavailable" &&
worker.state !== "stopped",
can_spawn_followup: false,
},
workdir_attachments: (worker.workdir_attachments ?? []).map((
attachment,
) => ({
@@ -38,10 +38,6 @@ function worker(overrides: Partial<Worker>): Worker {
kind: "runtime_worker",
display_hint: "Runtime Worker",
},
capabilities: {
can_stop: true,
can_spawn_followup: false,
},
workdir_attachments: [],
diagnostics: [],
...overrides,
@@ -56,10 +52,6 @@ Deno.test("registry-only workers are not sidebar targets or console targets", ()
kind: "backend_worker_registry",
display_hint: "Missing Worker",
},
capabilities: {
can_stop: false,
can_spawn_followup: false,
},
});
assertEquals(canShowWorkerInSidebar(registryOnly), false);
assertEquals(canOpenWorkerConsole(registryOnly), false);
@@ -1935,22 +1935,6 @@
</dd>
</div>
</dl>
<details class="metadata-details">
<summary>Capabilities</summary>
<ul>
<li>
stop: {worker.capabilities.can_stop
? "available"
: "unsupported"}
</li>
<li>
follow-up spawn: {worker.capabilities
.can_spawn_followup
? "available"
: "unsupported"}
</li>
</ul>
</details>
{:else if !workerError}
<p>Loading Worker detail…</p>
{/if}
@@ -62,10 +62,6 @@ function worker(workerId = "worker-a", runtimeId = "runtime-a"): Worker {
profile: "default",
tags: [],
diagnostics: [],
capabilities: {
can_stop: true,
can_spawn_followup: true,
},
workspace: { visibility: "workspace", identity: "workspace-a" },
implementation: { kind: "builtin", display_hint: "test" },
workdir_attachments: [],
@@ -1,5 +1,6 @@
import {
canDeleteSidebarWorker,
canStopSidebarWorker,
deleteSidebarWorker,
stopSidebarWorker,
} from "../../src/lib/workspace/sidebar/worker-actions.ts";
@@ -41,7 +42,6 @@ const worker = {
runtime_id: "runtime /",
worker_id: "worker /",
state: "running",
capabilities: { can_stop: true },
} as Worker;
function jsonResponse(payload: unknown, status = 200): Response {
@@ -51,6 +51,33 @@ function jsonResponse(payload: unknown, status = 200): Response {
});
}
Deno.test("sidebar Stop follows observed catalog lifecycle, not foreground display state", () => {
for (const lifecycleState of ["idle", "running", "paused"]) {
const target = {
availability: "observed" as const,
lifecycleState,
state: "unknown",
};
assert(canStopSidebarWorker(target));
assert(!canStopSidebarWorker({ ...target, availability: "unavailable" }));
}
for (
const lifecycleState of [
"stopped",
"missing",
"execution_unavailable",
"unknown",
]
) {
const target = {
availability: "observed" as const,
lifecycleState,
state: "running",
};
assert(!canStopSidebarWorker(target));
}
});
Deno.test("sidebar Stop uses the workspace-scoped Worker lifecycle endpoint", async () => {
const requests: Array<{ url: string; init?: RequestInit }> = [];
const fetchFn = (async (input: RequestInfo | URL, init?: RequestInit) => {