runtime: preserve typed workdir errors
This commit is contained in:
parent
72c7beb9c7
commit
da0fe7c81f
|
|
@ -1,8 +1,8 @@
|
||||||
---
|
---
|
||||||
title: 'Preserve typed Workdir not-found errors'
|
title: 'Preserve typed Workdir not-found errors'
|
||||||
state: 'inprogress'
|
state: 'done'
|
||||||
created_at: '2026-07-13T10:58:32Z'
|
created_at: '2026-07-13T10:58:32Z'
|
||||||
updated_at: '2026-07-13T10:59:02Z'
|
updated_at: '2026-07-13T11:16:48Z'
|
||||||
assignee: null
|
assignee: null
|
||||||
queued_by: 'yoi ticket'
|
queued_by: 'yoi ticket'
|
||||||
queued_at: '2026-07-13T10:59:02Z'
|
queued_at: '2026-07-13T10:59:02Z'
|
||||||
|
|
|
||||||
|
|
@ -39,4 +39,37 @@ Ticket を `yoi ticket` が queued にしました。
|
||||||
State changed to `inprogress`.
|
State changed to `inprogress`.
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: implementation_report author: hare at: 2026-07-13T11:16:48Z -->
|
||||||
|
|
||||||
|
## Implementation report
|
||||||
|
|
||||||
|
Removed text/substring-based Workdir not-found handling.
|
||||||
|
|
||||||
|
- Added `RuntimeError::WorkingDirectory(WorkingDirectoryDiagnostic)` so worker-runtime preserves typed Workdir diagnostic codes instead of flattening them into `invalid_request`.
|
||||||
|
- Worker Runtime REST errors now return Workdir diagnostic codes such as `working_directory_not_found` as `error.code` and map that code to HTTP 404.
|
||||||
|
- Remote Runtime HTTP error mapping now preserves typed error codes from Runtime error bodies instead of remapping all HTTP 404 responses to a generic worker-not-found code.
|
||||||
|
- Embedded Runtime diagnostics also preserve typed Workdir diagnostic codes.
|
||||||
|
- `workdir_status_from_runtime_miss` now uses exact `diagnostic.code == "working_directory_not_found"` rather than substring matching.
|
||||||
|
- Added tests for preserving Workdir REST error codes and for exact Workdir miss classification.
|
||||||
|
|
||||||
|
Validation:
|
||||||
|
- `cargo fmt --check`
|
||||||
|
- `git diff --check`
|
||||||
|
- `cargo check -q`
|
||||||
|
- `cargo test -q -p worker-runtime --features fs-store,ws-server`
|
||||||
|
- `cargo test -q -p yoi-workspace-server`
|
||||||
|
- `nix build .#yoi --no-link`
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: state_changed author: "yoi ticket" at: 2026-07-13T11:16:48Z from: inprogress to: done reason: cli_state field: state -->
|
||||||
|
|
||||||
|
## State changed
|
||||||
|
|
||||||
|
State changed to `done`.
|
||||||
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
use crate::execution::WorkerExecutionResult;
|
use crate::execution::WorkerExecutionResult;
|
||||||
use crate::identity::WorkerId;
|
use crate::identity::WorkerId;
|
||||||
|
use crate::working_directory::WorkingDirectoryDiagnostic;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
/// Errors returned by the embedded Runtime API.
|
/// Errors returned by the embedded Runtime API.
|
||||||
|
|
@ -38,6 +39,9 @@ pub enum RuntimeError {
|
||||||
#[error("invalid request: {0}")]
|
#[error("invalid request: {0}")]
|
||||||
InvalidRequest(String),
|
InvalidRequest(String),
|
||||||
|
|
||||||
|
#[error(transparent)]
|
||||||
|
WorkingDirectory(#[from] WorkingDirectoryDiagnostic),
|
||||||
|
|
||||||
#[error("config bundle `{bundle_id}` was not found")]
|
#[error("config bundle `{bundle_id}` was not found")]
|
||||||
ConfigBundleMissing { bundle_id: String },
|
ConfigBundleMissing { bundle_id: String },
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -761,15 +761,15 @@ async fn require_local_token(
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
struct RuntimeHttpRestError {
|
struct RuntimeHttpRestError {
|
||||||
status: StatusCode,
|
status: StatusCode,
|
||||||
code: &'static str,
|
code: String,
|
||||||
message: String,
|
message: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RuntimeHttpRestError {
|
impl RuntimeHttpRestError {
|
||||||
fn new(status: StatusCode, code: &'static str, message: impl Into<String>) -> Self {
|
fn new(status: StatusCode, code: impl Into<String>, message: impl Into<String>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
status,
|
status,
|
||||||
code,
|
code: code.into(),
|
||||||
message: message.into(),
|
message: message.into(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -801,7 +801,7 @@ impl IntoResponse for RuntimeHttpRestError {
|
||||||
fn into_response(self) -> Response {
|
fn into_response(self) -> Response {
|
||||||
let body = RuntimeHttpErrorResponse {
|
let body = RuntimeHttpErrorResponse {
|
||||||
error: RuntimeHttpErrorDetail {
|
error: RuntimeHttpErrorDetail {
|
||||||
code: self.code.to_string(),
|
code: self.code,
|
||||||
message: self.message,
|
message: self.message,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
@ -814,6 +814,11 @@ fn status_for_runtime_error(error: &RuntimeError) -> StatusCode {
|
||||||
RuntimeError::WorkerNotFound { .. } | RuntimeError::ConfigBundleMissing { .. } => {
|
RuntimeError::WorkerNotFound { .. } | RuntimeError::ConfigBundleMissing { .. } => {
|
||||||
StatusCode::NOT_FOUND
|
StatusCode::NOT_FOUND
|
||||||
}
|
}
|
||||||
|
RuntimeError::WorkingDirectory(diagnostic)
|
||||||
|
if diagnostic.code == "working_directory_not_found" =>
|
||||||
|
{
|
||||||
|
StatusCode::NOT_FOUND
|
||||||
|
}
|
||||||
RuntimeError::RuntimeStopped
|
RuntimeError::RuntimeStopped
|
||||||
| RuntimeError::WorkerExecutionUnavailable { .. }
|
| RuntimeError::WorkerExecutionUnavailable { .. }
|
||||||
| RuntimeError::ExecutionBackendUnavailable { .. }
|
| RuntimeError::ExecutionBackendUnavailable { .. }
|
||||||
|
|
@ -823,7 +828,8 @@ fn status_for_runtime_error(error: &RuntimeError) -> StatusCode {
|
||||||
| RuntimeError::InvalidInitialInputKind { .. }
|
| RuntimeError::InvalidInitialInputKind { .. }
|
||||||
| RuntimeError::ConfigBundleDigestMismatch { .. }
|
| RuntimeError::ConfigBundleDigestMismatch { .. }
|
||||||
| RuntimeError::InvalidProfileSelector { .. }
|
| RuntimeError::InvalidProfileSelector { .. }
|
||||||
| RuntimeError::UnsupportedConfigDeclaration { .. } => StatusCode::BAD_REQUEST,
|
| RuntimeError::UnsupportedConfigDeclaration { .. }
|
||||||
|
| RuntimeError::WorkingDirectory(_) => StatusCode::BAD_REQUEST,
|
||||||
RuntimeError::StoreIo { .. }
|
RuntimeError::StoreIo { .. }
|
||||||
| RuntimeError::StoreMissing { .. }
|
| RuntimeError::StoreMissing { .. }
|
||||||
| RuntimeError::StoreCorrupt { .. }
|
| RuntimeError::StoreCorrupt { .. }
|
||||||
|
|
@ -831,24 +837,33 @@ fn status_for_runtime_error(error: &RuntimeError) -> StatusCode {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn code_for_runtime_error(error: &RuntimeError) -> &'static str {
|
fn code_for_runtime_error(error: &RuntimeError) -> String {
|
||||||
match error {
|
match error {
|
||||||
RuntimeError::RuntimeStopped => "runtime_stopped",
|
RuntimeError::RuntimeStopped => "runtime_stopped".to_string(),
|
||||||
RuntimeError::WorkerNotFound { .. } => "worker_not_found",
|
RuntimeError::WorkerNotFound { .. } => "worker_not_found".to_string(),
|
||||||
RuntimeError::WorkerExecutionUnavailable { .. } => "worker_execution_unavailable",
|
RuntimeError::WorkerExecutionUnavailable { .. } => {
|
||||||
RuntimeError::ExecutionBackendUnavailable { .. } => "execution_backend_unavailable",
|
"worker_execution_unavailable".to_string()
|
||||||
RuntimeError::WorkerExecutionRejected { .. } => "worker_execution_rejected",
|
}
|
||||||
RuntimeError::LimitTooLarge { .. } => "limit_too_large",
|
RuntimeError::ExecutionBackendUnavailable { .. } => {
|
||||||
RuntimeError::InvalidRequest(_) => "invalid_request",
|
"execution_backend_unavailable".to_string()
|
||||||
RuntimeError::InvalidInitialInputKind { .. } => "invalid_initial_input_kind",
|
}
|
||||||
RuntimeError::ConfigBundleMissing { .. } => "config_bundle_missing",
|
RuntimeError::WorkerExecutionRejected { .. } => "worker_execution_rejected".to_string(),
|
||||||
RuntimeError::ConfigBundleDigestMismatch { .. } => "config_bundle_digest_mismatch",
|
RuntimeError::LimitTooLarge { .. } => "limit_too_large".to_string(),
|
||||||
RuntimeError::InvalidProfileSelector { .. } => "invalid_profile_selector",
|
RuntimeError::InvalidRequest(_) => "invalid_request".to_string(),
|
||||||
RuntimeError::UnsupportedConfigDeclaration { .. } => "unsupported_config_declaration",
|
RuntimeError::WorkingDirectory(diagnostic) => diagnostic.code.clone(),
|
||||||
RuntimeError::StoreIo { .. } => "store_io",
|
RuntimeError::InvalidInitialInputKind { .. } => "invalid_initial_input_kind".to_string(),
|
||||||
RuntimeError::StoreMissing { .. } => "store_missing",
|
RuntimeError::ConfigBundleMissing { .. } => "config_bundle_missing".to_string(),
|
||||||
RuntimeError::StoreCorrupt { .. } => "store_corrupt",
|
RuntimeError::ConfigBundleDigestMismatch { .. } => {
|
||||||
RuntimeError::StatePoisoned => "state_poisoned",
|
"config_bundle_digest_mismatch".to_string()
|
||||||
|
}
|
||||||
|
RuntimeError::InvalidProfileSelector { .. } => "invalid_profile_selector".to_string(),
|
||||||
|
RuntimeError::UnsupportedConfigDeclaration { .. } => {
|
||||||
|
"unsupported_config_declaration".to_string()
|
||||||
|
}
|
||||||
|
RuntimeError::StoreIo { .. } => "store_io".to_string(),
|
||||||
|
RuntimeError::StoreMissing { .. } => "store_missing".to_string(),
|
||||||
|
RuntimeError::StoreCorrupt { .. } => "store_corrupt".to_string(),
|
||||||
|
RuntimeError::StatePoisoned => "state_poisoned".to_string(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1125,6 +1140,21 @@ mod tests {
|
||||||
assert_eq!(error.error.code, "worker_not_found");
|
assert_eq!(error.error.code, "worker_not_found");
|
||||||
assert!(error.error.message.contains("999"));
|
assert!(error.error.message.contains("999"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn workdir_runtime_errors_preserve_diagnostic_code() {
|
||||||
|
let error =
|
||||||
|
RuntimeError::WorkingDirectory(crate::working_directory::WorkingDirectoryDiagnostic {
|
||||||
|
code: "working_directory_not_found".to_string(),
|
||||||
|
message: "working directory missing-workdir was not found".to_string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
assert_eq!(status_for_runtime_error(&error), StatusCode::NOT_FOUND);
|
||||||
|
assert_eq!(
|
||||||
|
code_for_runtime_error(&error),
|
||||||
|
"working_directory_not_found"
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(all(test, feature = "ws-server"))]
|
#[cfg(all(test, feature = "ws-server"))]
|
||||||
|
|
|
||||||
|
|
@ -233,7 +233,7 @@ impl Runtime {
|
||||||
};
|
};
|
||||||
backend
|
backend
|
||||||
.create_working_directory(&request)
|
.create_working_directory(&request)
|
||||||
.map_err(|diagnostic| RuntimeError::InvalidRequest(diagnostic.to_string()))
|
.map_err(RuntimeError::from)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// List Runtime-owned working directories through the attached execution backend.
|
/// List Runtime-owned working directories through the attached execution backend.
|
||||||
|
|
@ -269,7 +269,7 @@ impl Runtime {
|
||||||
};
|
};
|
||||||
let status = backend
|
let status = backend
|
||||||
.working_directory(working_directory_id)
|
.working_directory(working_directory_id)
|
||||||
.map_err(|diagnostic| RuntimeError::InvalidRequest(diagnostic.to_string()))?;
|
.map_err(RuntimeError::from)?;
|
||||||
self.annotate_working_directory_status(status)
|
self.annotate_working_directory_status(status)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -294,7 +294,7 @@ impl Runtime {
|
||||||
};
|
};
|
||||||
backend
|
backend
|
||||||
.cleanup_working_directory(working_directory_id)
|
.cleanup_working_directory(working_directory_id)
|
||||||
.map_err(|diagnostic| RuntimeError::InvalidRequest(diagnostic.to_string()))
|
.map_err(RuntimeError::from)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn annotate_working_directory_statuses(
|
fn annotate_working_directory_statuses(
|
||||||
|
|
|
||||||
|
|
@ -2144,7 +2144,7 @@ impl WorkspaceWorkerRuntime for RemoteWorkerRuntime {
|
||||||
worker: Some(self.map_worker_detail(response.worker)),
|
worker: Some(self.map_worker_detail(response.worker)),
|
||||||
diagnostics: Vec::new(),
|
diagnostics: Vec::new(),
|
||||||
},
|
},
|
||||||
Err(diagnostic) if diagnostic.code == "remote_worker_not_found" => WorkerLookupResult {
|
Err(diagnostic) if diagnostic.code == "worker_not_found" => WorkerLookupResult {
|
||||||
worker: None,
|
worker: None,
|
||||||
diagnostics: Vec::new(),
|
diagnostics: Vec::new(),
|
||||||
},
|
},
|
||||||
|
|
@ -2860,6 +2860,11 @@ fn embedded_runtime_diagnostic(error: &EmbeddedRuntimeError) -> RuntimeDiagnosti
|
||||||
DiagnosticSeverity::Warning,
|
DiagnosticSeverity::Warning,
|
||||||
error.to_string(),
|
error.to_string(),
|
||||||
),
|
),
|
||||||
|
EmbeddedRuntimeError::WorkingDirectory(workdir_diagnostic) => diagnostic(
|
||||||
|
workdir_diagnostic.code.clone(),
|
||||||
|
DiagnosticSeverity::Warning,
|
||||||
|
workdir_diagnostic.message.clone(),
|
||||||
|
),
|
||||||
EmbeddedRuntimeError::InvalidRequest(_)
|
EmbeddedRuntimeError::InvalidRequest(_)
|
||||||
| EmbeddedRuntimeError::ConfigBundleMissing { .. }
|
| EmbeddedRuntimeError::ConfigBundleMissing { .. }
|
||||||
| EmbeddedRuntimeError::ConfigBundleDigestMismatch { .. }
|
| EmbeddedRuntimeError::ConfigBundleDigestMismatch { .. }
|
||||||
|
|
@ -2989,7 +2994,8 @@ fn remote_http_status_diagnostic(
|
||||||
StatusCode::UNAUTHORIZED | StatusCode::FORBIDDEN => {
|
StatusCode::UNAUTHORIZED | StatusCode::FORBIDDEN => {
|
||||||
("remote_runtime_auth_failed", DiagnosticSeverity::Error)
|
("remote_runtime_auth_failed", DiagnosticSeverity::Error)
|
||||||
}
|
}
|
||||||
StatusCode::NOT_FOUND => ("remote_worker_not_found", DiagnosticSeverity::Warning),
|
_ if error.is_some() => (remote_code, DiagnosticSeverity::Warning),
|
||||||
|
StatusCode::NOT_FOUND => ("remote_runtime_not_found", DiagnosticSeverity::Warning),
|
||||||
StatusCode::METHOD_NOT_ALLOWED | StatusCode::NOT_IMPLEMENTED => {
|
StatusCode::METHOD_NOT_ALLOWED | StatusCode::NOT_IMPLEMENTED => {
|
||||||
("remote_runtime_unsupported", DiagnosticSeverity::Warning)
|
("remote_runtime_unsupported", DiagnosticSeverity::Warning)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4290,7 +4290,7 @@ fn sync_runtime_workdir_observations(
|
||||||
fn workdir_status_from_runtime_miss(diagnostics: &[RuntimeDiagnostic]) -> &'static str {
|
fn workdir_status_from_runtime_miss(diagnostics: &[RuntimeDiagnostic]) -> &'static str {
|
||||||
if diagnostics
|
if diagnostics
|
||||||
.iter()
|
.iter()
|
||||||
.any(|diagnostic| diagnostic.code.contains("not_found"))
|
.any(|diagnostic| diagnostic.code == "working_directory_not_found")
|
||||||
{
|
{
|
||||||
"not_found"
|
"not_found"
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -5211,6 +5211,26 @@ mod tests {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn workdir_runtime_miss_uses_exact_typed_code() {
|
||||||
|
assert_eq!(
|
||||||
|
workdir_status_from_runtime_miss(&[RuntimeDiagnostic {
|
||||||
|
code: "working_directory_not_found".to_string(),
|
||||||
|
severity: DiagnosticSeverity::Warning,
|
||||||
|
message: "missing".to_string(),
|
||||||
|
}]),
|
||||||
|
"not_found"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
workdir_status_from_runtime_miss(&[RuntimeDiagnostic {
|
||||||
|
code: "some_other_not_found".to_string(),
|
||||||
|
severity: DiagnosticSeverity::Warning,
|
||||||
|
message: "not a typed workdir miss".to_string(),
|
||||||
|
}]),
|
||||||
|
"unknown"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn profile_settings_api_returns_typed_diagnostics_for_duplicate_selector() {
|
async fn profile_settings_api_returns_typed_diagnostics_for_duplicate_selector() {
|
||||||
let dir = tempfile::tempdir().unwrap();
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user