feat: transfer typed startup causes over Runtime HTTP with real registration fixture

This commit is contained in:
2026-11-04 15:50:30 +09:00
parent c4a34cf0cc
commit 7873f4359a
8 changed files with 543 additions and 7 deletions
+16
View File
@@ -105,10 +105,25 @@ pub struct RuntimeApiError {
status: u16, status: u16,
} }
/// Transient, allowlisted startup observations copied from the execution owner.
/// This is not an operation result, lifecycle record, or permission to retry.
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct WorkerStartupFailure {
pub primary: StartupFailureAttachment,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub secondary: Vec<StartupSecondaryFailure>,
pub resources: Option<StartupResourceFacts>,
pub persistence: Option<StartupPersistenceOutcome>,
pub io_kind: Option<StartupIoKind>,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct RuntimeApiErrorDetail { pub struct RuntimeApiErrorDetail {
pub code: String, pub code: String,
pub message: String, pub message: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub startup: Option<WorkerStartupFailure>,
} }
impl RuntimeApiError { impl RuntimeApiError {
@@ -117,6 +132,7 @@ impl RuntimeApiError {
error: RuntimeApiErrorDetail { error: RuntimeApiErrorDetail {
code: code.into(), code: code.into(),
message: message.into(), message: message.into(),
startup: None,
}, },
status, status,
} }
+38 -1
View File
@@ -542,6 +542,8 @@ pub struct RuntimeHttpErrorResponse {
pub struct RuntimeHttpErrorDetail { pub struct RuntimeHttpErrorDetail {
pub code: String, pub code: String,
pub message: String, pub message: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub startup: Option<runtime_api::WorkerStartupFailure>,
} }
#[cfg(feature = "ws-server")] #[cfg(feature = "ws-server")]
@@ -2325,6 +2327,7 @@ struct RuntimeHttpRestError {
status: StatusCode, status: StatusCode,
code: String, code: String,
message: String, message: String,
startup: Option<runtime_api::WorkerStartupFailure>,
} }
impl RuntimeHttpRestError { impl RuntimeHttpRestError {
@@ -2333,13 +2336,28 @@ impl RuntimeHttpRestError {
status, status,
code: code.into(), code: code.into(),
message: message.into(), message: message.into(),
startup: None,
} }
} }
fn runtime(error: RuntimeError) -> Self { fn runtime(error: RuntimeError) -> Self {
let status = status_for_runtime_error(&error); let status = status_for_runtime_error(&error);
let code = code_for_runtime_error(&error); let code = code_for_runtime_error(&error);
Self::new(status, code, error.to_string()) let startup = match &error {
RuntimeError::WorkerExecutionRejected { result, .. } => startup_transfer(result),
_ => None,
};
let message = if startup.is_some() {
"Worker startup failed".to_owned()
} else {
error.to_string()
};
Self {
status,
code,
message,
startup,
}
} }
fn working_directory_join(_error: tokio::task::JoinError) -> Self { fn working_directory_join(_error: tokio::task::JoinError) -> Self {
@@ -2375,12 +2393,31 @@ impl IntoResponse for RuntimeHttpRestError {
error: RuntimeHttpErrorDetail { error: RuntimeHttpErrorDetail {
code: self.code, code: self.code,
message: self.message, message: self.message,
startup: self.startup,
}, },
}; };
(self.status, Json(body)).into_response() (self.status, Json(body)).into_response()
} }
} }
/// Explicit transfer mapping, not another execution/lifecycle result. HTTP request
/// authentication has already established this internal Runtime caller's scope.
fn startup_transfer(
result: &crate::execution::WorkerExecutionResult,
) -> Option<runtime_api::WorkerStartupFailure> {
let primary = worker::startup_failure::reproject_internal_startup_failure(
result.startup_failure.as_ref()?,
)
.ok()?;
Some(runtime_api::WorkerStartupFailure {
primary,
secondary: result.secondary_failures.clone(),
resources: result.startup_resources,
persistence: result.startup_failure_persistence,
io_kind: result.startup_io_kind,
})
}
fn status_for_runtime_error(error: &RuntimeError) -> StatusCode { fn status_for_runtime_error(error: &RuntimeError) -> StatusCode {
match error { match error {
RuntimeError::WorkerNotFound { .. } | RuntimeError::ConfigBundleMissing { .. } => { RuntimeError::WorkerNotFound { .. } | RuntimeError::ConfigBundleMissing { .. } => {
@@ -224,7 +224,10 @@ where
} }
fn api_error(error: RuntimeHttpRestError) -> runtime_api::RuntimeApiError { fn api_error(error: RuntimeHttpRestError) -> runtime_api::RuntimeApiError {
runtime_api::RuntimeApiError::new(error.status.as_u16(), error.code, error.message) let mut response =
runtime_api::RuntimeApiError::new(error.status.as_u16(), error.code, error.message);
response.error.startup = error.startup;
response
} }
impl runtime_api::RuntimeApi for RuntimeManagementApi { impl runtime_api::RuntimeApi for RuntimeManagementApi {
+14
View File
@@ -106,6 +106,20 @@ pub fn runtime_startup_attachment(
StartupFailureAttachment::from_projected(&projected) StartupFailureAttachment::from_projected(&projected)
} }
/// The internal HTTP/Host transfer admits only implemented startup domains.
/// Unknown/corrupt envelopes cannot be blindly forwarded or classified from text.
pub fn reproject_internal_startup_failure(
failure: &StartupFailureAttachment,
) -> Result<StartupFailureAttachment, error_contract::ContractError> {
if let Ok(projected) = failure
.project_for::<InternalTransfer, protocol::session_failure::SessionRegistrationCause>(&())
{
return StartupFailureAttachment::from_projected(&projected);
}
let projected = failure.project_for::<InternalTransfer, RuntimeStartupCause>(&())?;
StartupFailureAttachment::from_projected(&projected)
}
/// Dispatch by the domain attested in process, not by parsing attachment fields. /// Dispatch by the domain attested in process, not by parsing attachment fields.
pub(crate) fn persist_binding_startup_failure<S: session_store::WorkerMetadataStore>( pub(crate) fn persist_binding_startup_failure<S: session_store::WorkerMetadataStore>(
store: &S, store: &S,
+38 -5
View File
@@ -117,6 +117,9 @@ where
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct RuntimeDiagnostic { pub struct RuntimeDiagnostic {
/// Internal startup observations; public consumers must separately authorize projection.
#[serde(skip)]
pub startup: Option<runtime_api::WorkerStartupFailure>,
pub code: String, pub code: String,
pub severity: HostDiagnosticSeverity, pub severity: HostDiagnosticSeverity,
pub message: String, pub message: String,
@@ -6649,7 +6652,7 @@ fn sanitize_embedded_execution_detail(summary: &str, message: &str) -> String {
} }
fn embedded_runtime_diagnostic(error: &EmbeddedRuntimeError) -> RuntimeDiagnostic { fn embedded_runtime_diagnostic(error: &EmbeddedRuntimeError) -> RuntimeDiagnostic {
match error { let mut projection = match error {
EmbeddedRuntimeError::RestoreAdmissionBusy { .. } => diagnostic( EmbeddedRuntimeError::RestoreAdmissionBusy { .. } => diagnostic(
"restore_admission_busy", "restore_admission_busy",
HostDiagnosticSeverity::Warning, HostDiagnosticSeverity::Warning,
@@ -6767,7 +6770,19 @@ fn embedded_runtime_diagnostic(error: &EmbeddedRuntimeError) -> RuntimeDiagnosti
HostDiagnosticSeverity::Error, HostDiagnosticSeverity::Error,
"Embedded Runtime state is unavailable".to_string(), "Embedded Runtime state is unavailable".to_string(),
), ),
};
if let EmbeddedRuntimeError::WorkerExecutionRejected { result, .. } = error {
projection.startup = result.startup_failure.as_ref().and_then(|primary| {
internal_startup_transfer(runtime_api::WorkerStartupFailure {
primary: primary.clone(),
secondary: result.secondary_failures.clone(),
resources: result.startup_resources,
persistence: result.startup_failure_persistence,
io_kind: result.startup_io_kind,
})
});
} }
projection
} }
fn host_id_for_embedded_workspace(workspace_id: &str) -> String { fn host_id_for_embedded_workspace(workspace_id: &str) -> String {
@@ -6846,6 +6861,7 @@ fn runtime_api_diagnostic(
) -> RuntimeDiagnostic { ) -> RuntimeDiagnostic {
match error { match error {
runtime_api::client_support::ClientError::Public { status, error } => { runtime_api::client_support::ClientError::Public { status, error } => {
let startup = error.error.startup.and_then(internal_startup_transfer);
let remote_code = error.error.code; let remote_code = error.error.code;
let message = sanitize_remote_runtime_message(&remote_code, &error.error.message); let message = sanitize_remote_runtime_message(&remote_code, &error.error.message);
let (code, severity) = match status { let (code, severity) = match status {
@@ -6862,13 +6878,15 @@ fn runtime_api_diagnostic(
}, },
), ),
}; };
diagnostic( let mut projection = diagnostic(
code, code,
severity, severity,
format!( format!(
"Remote Runtime '{runtime_id}' rejected request (HTTP {status}): {message}" "Remote Runtime '{runtime_id}' rejected request (HTTP {status}): {message}"
), ),
) );
projection.startup = startup;
projection
} }
runtime_api::client_support::ClientError::Failure(failure) => { runtime_api::client_support::ClientError::Failure(failure) => {
let code = match failure { let code = match failure {
@@ -6945,11 +6963,25 @@ fn remote_http_status_diagnostic(
} }
_ => (remote_code, HostDiagnosticSeverity::Warning), _ => (remote_code, HostDiagnosticSeverity::Warning),
}; };
diagnostic( let mut projection = diagnostic(
code, code,
severity, severity,
format!("Remote Runtime '{runtime_id}' rejected request (HTTP {status}): {remote_message}"), format!("Remote Runtime '{runtime_id}' rejected request (HTTP {status}): {remote_message}"),
) );
projection.startup = error
.and_then(|error| error.error.startup)
.and_then(internal_startup_transfer);
projection
}
/// Treat transferred details as untrusted observations and reproject through the
/// same domain allowlist; HTTP status and text never supply a missing cause.
fn internal_startup_transfer(
mut startup: runtime_api::WorkerStartupFailure,
) -> Option<runtime_api::WorkerStartupFailure> {
startup.primary =
worker::startup_failure::reproject_internal_startup_failure(&startup.primary).ok()?;
Some(startup)
} }
fn diagnostic( fn diagnostic(
@@ -6958,6 +6990,7 @@ fn diagnostic(
message: impl Into<String>, message: impl Into<String>,
) -> RuntimeDiagnostic { ) -> RuntimeDiagnostic {
RuntimeDiagnostic { RuntimeDiagnostic {
startup: None,
code: code.into(), code: code.into(),
severity, severity,
message: message.into(), message: message.into(),
+50
View File
@@ -8612,6 +8612,7 @@ fn worker_input_result_to_api(
// distinguishable without collapsing it into a definite refusal or replaying input. // distinguishable without collapsing it into a definite refusal or replaying input.
if result.disposition == WorkerInputDisposition::Unknown { if result.disposition == WorkerInputDisposition::Unknown {
result.diagnostics.push(RuntimeDiagnostic { result.diagnostics.push(RuntimeDiagnostic {
startup: None,
code: "worker_input_outcome_unknown".into(), code: "worker_input_outcome_unknown".into(),
severity: HostDiagnosticSeverity::Warning, severity: HostDiagnosticSeverity::Warning,
message: "Worker input delivery outcome is unknown; inspect the current session before retrying".into(), message: "Worker input delivery outcome is unknown; inspect the current session before retrying".into(),
@@ -27020,6 +27021,7 @@ fn skill_api_error(error: skills::SkillError) -> ApiError {
message: format!("unknown Skill `{name}`"), message: format!("unknown Skill `{name}`"),
}, },
vec![RuntimeDiagnostic { vec![RuntimeDiagnostic {
startup: None,
code: "skill_not_found".to_string(), code: "skill_not_found".to_string(),
severity: HostDiagnosticSeverity::Error, severity: HostDiagnosticSeverity::Error,
message: format!("unknown Skill `{name}`"), message: format!("unknown Skill `{name}`"),
@@ -30426,6 +30428,7 @@ fn prepare_workspace_workdir_create(
.to_string(), .to_string(),
), ),
vec![RuntimeDiagnostic { vec![RuntimeDiagnostic {
startup: None,
code: "default_runtime_not_configured".to_string(), code: "default_runtime_not_configured".to_string(),
severity: HostDiagnosticSeverity::Error, severity: HostDiagnosticSeverity::Error,
message: "Workspace default Runtime is not configured".to_string(), message: "Workspace default Runtime is not configured".to_string(),
@@ -30580,6 +30583,7 @@ async fn create_workspace_working_directory(
return Err(ApiError::with_diagnostics( return Err(ApiError::with_diagnostics(
Error::UnknownRuntime(reserved.resolved_runtime_id), Error::UnknownRuntime(reserved.resolved_runtime_id),
vec![RuntimeDiagnostic { vec![RuntimeDiagnostic {
startup: None,
code: "runtime_unavailable".to_string(), code: "runtime_unavailable".to_string(),
severity: HostDiagnosticSeverity::Error, severity: HostDiagnosticSeverity::Error,
message: "Selected Runtime is not available".to_string(), message: "Selected Runtime is not available".to_string(),
@@ -30603,6 +30607,7 @@ async fn create_workspace_working_directory(
message: "Selected Runtime is not connected".to_string(), message: "Selected Runtime is not connected".to_string(),
}, },
vec![RuntimeDiagnostic { vec![RuntimeDiagnostic {
startup: None,
code: "runtime_unavailable".to_string(), code: "runtime_unavailable".to_string(),
severity: HostDiagnosticSeverity::Error, severity: HostDiagnosticSeverity::Error,
message: "Selected Runtime is not available".to_string(), message: "Selected Runtime is not available".to_string(),
@@ -30659,6 +30664,7 @@ async fn create_workspace_working_directory(
return Err(ApiError::with_diagnostics( return Err(ApiError::with_diagnostics(
error.into_error(), error.into_error(),
vec![RuntimeDiagnostic { vec![RuntimeDiagnostic {
startup: None,
code: failure_code, code: failure_code,
severity: HostDiagnosticSeverity::Error, severity: HostDiagnosticSeverity::Error,
message: "Selected Runtime could not inspect the reserved Workdir".to_string(), message: "Selected Runtime could not inspect the reserved Workdir".to_string(),
@@ -30697,6 +30703,7 @@ async fn create_workspace_working_directory(
return Err(ApiError::with_diagnostics( return Err(ApiError::with_diagnostics(
error.into_error(), error.into_error(),
vec![RuntimeDiagnostic { vec![RuntimeDiagnostic {
startup: None,
code: failure_code, code: failure_code,
severity: HostDiagnosticSeverity::Error, severity: HostDiagnosticSeverity::Error,
message: "Selected Runtime rejected Workdir creation".to_string(), message: "Selected Runtime rejected Workdir creation".to_string(),
@@ -30736,6 +30743,7 @@ async fn create_workspace_working_directory(
diagnostics.insert( diagnostics.insert(
0, 0,
RuntimeDiagnostic { RuntimeDiagnostic {
startup: None,
code: failure_code.clone(), code: failure_code.clone(),
severity: HostDiagnosticSeverity::Error, severity: HostDiagnosticSeverity::Error,
message: "Runtime did not create the reserved Workdir".to_string(), message: "Runtime did not create the reserved Workdir".to_string(),
@@ -31490,6 +31498,7 @@ fn build_runtime_cleanup_plan(
let mut diagnostics = error.diagnostics; let mut diagnostics = error.diagnostics;
if diagnostics.is_empty() { if diagnostics.is_empty() {
diagnostics.push(RuntimeDiagnostic { diagnostics.push(RuntimeDiagnostic {
startup: None,
code: "workspace_cleanup_runtime_observation_unavailable".to_string(), code: "workspace_cleanup_runtime_observation_unavailable".to_string(),
severity: HostDiagnosticSeverity::Warning, severity: HostDiagnosticSeverity::Warning,
message: sanitize_backend_error(&error.error.to_string()), message: sanitize_backend_error(&error.error.to_string()),
@@ -35599,6 +35608,7 @@ fn reject_workdir_for_embedded_runtime(runtime_id: &str, has_workdir: bool) -> A
message: "The embedded Runtime does not accept working directories".to_string(), message: "The embedded Runtime does not accept working directories".to_string(),
}, },
vec![RuntimeDiagnostic { vec![RuntimeDiagnostic {
startup: None,
code: "embedded_worker_workdir_unsupported".to_string(), code: "embedded_worker_workdir_unsupported".to_string(),
severity: HostDiagnosticSeverity::Error, severity: HostDiagnosticSeverity::Error,
message: "Choose a non-embedded Runtime for workspace-file Workers; embedded Workers are no-workdir Workspace-API workers.".to_string(), message: "Choose a non-embedded Runtime for workspace-file Workers; embedded Workers are no-workdir Workspace-API workers.".to_string(),
@@ -35689,6 +35699,7 @@ fn finalize_worker_spawn_stage<T>(
.map(|assignment| assignment.operation_id.as_str()) .map(|assignment| assignment.operation_id.as_str())
.unwrap_or("none"); .unwrap_or("none");
let mut diagnostics = vec![RuntimeDiagnostic { let mut diagnostics = vec![RuntimeDiagnostic {
startup: None,
code: format!("worker_spawn_finalize_{}_failed", stage.as_str()), code: format!("worker_spawn_finalize_{}_failed", stage.as_str()),
severity: HostDiagnosticSeverity::Error, severity: HostDiagnosticSeverity::Error,
message: format!( message: format!(
@@ -35704,6 +35715,7 @@ fn finalize_worker_spawn_stage<T>(
diagnostics.extend(compensation_errors); diagnostics.extend(compensation_errors);
if !compensation_failed { if !compensation_failed {
diagnostics.push(RuntimeDiagnostic { diagnostics.push(RuntimeDiagnostic {
startup: None,
code: "worker_spawn_compensated".to_string(), code: "worker_spawn_compensated".to_string(),
severity: HostDiagnosticSeverity::Info, severity: HostDiagnosticSeverity::Info,
message: format!( message: format!(
@@ -36039,6 +36051,7 @@ fn lifecycle_failure_detail(
fn spawn_compensation_diagnostic(code: &str, message: String) -> RuntimeDiagnostic { fn spawn_compensation_diagnostic(code: &str, message: String) -> RuntimeDiagnostic {
RuntimeDiagnostic { RuntimeDiagnostic {
startup: None,
code: code.to_string(), code: code.to_string(),
severity: HostDiagnosticSeverity::Error, severity: HostDiagnosticSeverity::Error,
message, message,
@@ -37044,6 +37057,7 @@ fn project_workspace_worker_with_attachments(
summary.state = "unavailable".to_string(); summary.state = "unavailable".to_string();
summary.worker_state = None; summary.worker_state = None;
summary.diagnostics.push(RuntimeDiagnostic { summary.diagnostics.push(RuntimeDiagnostic {
startup: None,
code: "worker_singleton_fenced".to_string(), code: "worker_singleton_fenced".to_string(),
severity: HostDiagnosticSeverity::Warning, severity: HostDiagnosticSeverity::Warning,
message: "This historical singleton generation is fenced by Backend ownership." message: "This historical singleton generation is fenced by Backend ownership."
@@ -37749,6 +37763,7 @@ fn working_directory_summaries(
} }
if let Some(grant_id) = legacy_external_workdir_grant_id(api, &record)? { if let Some(grant_id) = legacy_external_workdir_grant_id(api, &record)? {
diagnostics.push(RuntimeDiagnostic { diagnostics.push(RuntimeDiagnostic {
startup: None,
code: "external_workdir_legacy_permissions".to_string(), code: "external_workdir_legacy_permissions".to_string(),
severity: HostDiagnosticSeverity::Warning, severity: HostDiagnosticSeverity::Warning,
message: format!( message: format!(
@@ -37955,6 +37970,7 @@ fn worker_summary_from_registry(record: &WorkerRegistryRecord) -> InternalWorker
}, },
workdir_attachments: Vec::new(), workdir_attachments: Vec::new(),
diagnostics: vec![RuntimeDiagnostic { diagnostics: vec![RuntimeDiagnostic {
startup: None,
code: "backend_worker_missing".to_string(), code: "backend_worker_missing".to_string(),
severity: HostDiagnosticSeverity::Info, severity: HostDiagnosticSeverity::Info,
message: message:
@@ -37999,6 +38015,7 @@ fn worker_summary_from_projection(
Vec::new() Vec::new()
} else { } else {
vec![RuntimeDiagnostic { vec![RuntimeDiagnostic {
startup: None,
code: "worker_observation_unavailable".to_string(), code: "worker_observation_unavailable".to_string(),
severity: HostDiagnosticSeverity::Info, severity: HostDiagnosticSeverity::Info,
message: "The Worker remains in the Backend catalog, but its Runtime observation is currently unavailable.".to_string(), message: "The Worker remains in the Backend catalog, but its Runtime observation is currently unavailable.".to_string(),
@@ -38645,6 +38662,7 @@ fn validate_working_directory_claims_for_browser(
message: "working directory relative_cwd must stay inside the Runtime-owned working directory".to_string(), message: "working directory relative_cwd must stay inside the Runtime-owned working directory".to_string(),
}, },
vec![RuntimeDiagnostic { vec![RuntimeDiagnostic {
startup: None,
code: "working_directory_relative_cwd_invalid".to_string(), code: "working_directory_relative_cwd_invalid".to_string(),
severity: HostDiagnosticSeverity::Error, severity: HostDiagnosticSeverity::Error,
message: "relative_cwd must be a relative path without parent traversal".to_string(), message: "relative_cwd must be a relative path without parent traversal".to_string(),
@@ -39239,6 +39257,7 @@ fn settings_diagnostic(
message: impl Into<String>, message: impl Into<String>,
) -> RuntimeDiagnostic { ) -> RuntimeDiagnostic {
RuntimeDiagnostic { RuntimeDiagnostic {
startup: None,
code: code.into(), code: code.into(),
severity, severity,
message: message.into(), message: message.into(),
@@ -39260,6 +39279,7 @@ fn repository_lookup<T>(result: std::result::Result<T, RepositoryLookupError>) -
message: message.clone(), message: message.clone(),
}, },
vec![RuntimeDiagnostic { vec![RuntimeDiagnostic {
startup: None,
code: "repository_not_configured".to_string(), code: "repository_not_configured".to_string(),
severity: HostDiagnosticSeverity::Error, severity: HostDiagnosticSeverity::Error,
message, message,
@@ -39277,6 +39297,7 @@ fn repository_lookup<T>(result: std::result::Result<T, RepositoryLookupError>) -
message: message.clone(), message: message.clone(),
}, },
vec![RuntimeDiagnostic { vec![RuntimeDiagnostic {
startup: None,
code: "repository_provider_unsupported".to_string(), code: "repository_provider_unsupported".to_string(),
severity: HostDiagnosticSeverity::Error, severity: HostDiagnosticSeverity::Error,
message, message,
@@ -39292,6 +39313,7 @@ fn repository_lookup<T>(result: std::result::Result<T, RepositoryLookupError>) -
message: message.clone(), message: message.clone(),
}, },
vec![RuntimeDiagnostic { vec![RuntimeDiagnostic {
startup: None,
code: "repository_evidence_invalid".to_string(), code: "repository_evidence_invalid".to_string(),
severity: HostDiagnosticSeverity::Error, severity: HostDiagnosticSeverity::Error,
message, message,
@@ -39452,6 +39474,7 @@ fn workspace_id_mismatch_error() -> ApiError {
ApiError::with_diagnostics( ApiError::with_diagnostics(
Error::WorkspaceIdMismatch, Error::WorkspaceIdMismatch,
vec![RuntimeDiagnostic { vec![RuntimeDiagnostic {
startup: None,
code: "workspace_id_mismatch".to_string(), code: "workspace_id_mismatch".to_string(),
severity: HostDiagnosticSeverity::Error, severity: HostDiagnosticSeverity::Error,
message, message,
@@ -39561,6 +39584,7 @@ impl ApiErrorLog {
.diagnostics .diagnostics
.iter() .iter()
.map(|diagnostic| RuntimeDiagnostic { .map(|diagnostic| RuntimeDiagnostic {
startup: None,
code: diagnostic.code.clone(), code: diagnostic.code.clone(),
severity: match diagnostic.severity { severity: match diagnostic.severity {
server_api::DiagnosticSeverity::Info => HostDiagnosticSeverity::Info, server_api::DiagnosticSeverity::Info => HostDiagnosticSeverity::Info,
@@ -39598,6 +39622,7 @@ impl From<Error> for ApiError {
Error::DbCapacityExceeded Error::DbCapacityExceeded
| Error::DbExecutorUnavailable | Error::DbExecutorUnavailable
| Error::DbOperationOutcomeUnknown => vec![RuntimeDiagnostic { | Error::DbOperationOutcomeUnknown => vec![RuntimeDiagnostic {
startup: None,
code: match &error { code: match &error {
Error::DbCapacityExceeded => "db_capacity_exceeded", Error::DbCapacityExceeded => "db_capacity_exceeded",
Error::DbExecutorUnavailable => "db_executor_unavailable", Error::DbExecutorUnavailable => "db_executor_unavailable",
@@ -39608,21 +39633,25 @@ impl From<Error> for ApiError {
message: error.to_string(), message: error.to_string(),
}], }],
Error::WorkdirSessionOpenFailed { transport, .. } => vec![RuntimeDiagnostic { Error::WorkdirSessionOpenFailed { transport, .. } => vec![RuntimeDiagnostic {
startup: None,
code: "workdir_session_open_failed".to_string(), code: "workdir_session_open_failed".to_string(),
severity: HostDiagnosticSeverity::Error, severity: HostDiagnosticSeverity::Error,
message: transport.message.clone(), message: transport.message.clone(),
}], }],
Error::RuntimeOperationFailed { code, message, .. } => vec![RuntimeDiagnostic { Error::RuntimeOperationFailed { code, message, .. } => vec![RuntimeDiagnostic {
startup: None,
code: code.clone(), code: code.clone(),
severity: HostDiagnosticSeverity::Error, severity: HostDiagnosticSeverity::Error,
message: sanitize_backend_error(message), message: sanitize_backend_error(message),
}], }],
Error::WorkspaceSigningIdentity { code, message } => vec![RuntimeDiagnostic { Error::WorkspaceSigningIdentity { code, message } => vec![RuntimeDiagnostic {
startup: None,
code: code.clone(), code: code.clone(),
severity: HostDiagnosticSeverity::Error, severity: HostDiagnosticSeverity::Error,
message: sanitize_backend_error(message), message: sanitize_backend_error(message),
}], }],
Error::Ticket(ticket_error) => vec![RuntimeDiagnostic { Error::Ticket(ticket_error) => vec![RuntimeDiagnostic {
startup: None,
code: match ticket_error { code: match ticket_error {
ticket::TicketError::NotFound(_) => "ticket_not_found", ticket::TicketError::NotFound(_) => "ticket_not_found",
ticket::TicketError::Ambiguous { .. } => "ticket_ambiguous", ticket::TicketError::Ambiguous { .. } => "ticket_ambiguous",
@@ -39669,6 +39698,7 @@ impl From<Error> for ApiError {
subjektiv_diagnostic_code(message) subjektiv_diagnostic_code(message)
.map(|code| { .map(|code| {
vec![RuntimeDiagnostic { vec![RuntimeDiagnostic {
startup: None,
code: code.to_string(), code: code.to_string(),
severity: HostDiagnosticSeverity::Error, severity: HostDiagnosticSeverity::Error,
message: sanitize_backend_error(message), message: sanitize_backend_error(message),
@@ -43074,6 +43104,7 @@ mod tests {
worker: None, worker: None,
acceptance_evidence: Vec::new(), acceptance_evidence: Vec::new(),
diagnostics: vec![RuntimeDiagnostic { diagnostics: vec![RuntimeDiagnostic {
startup: None,
code: "fixture_runtime_rejected".to_string(), code: "fixture_runtime_rejected".to_string(),
severity: HostDiagnosticSeverity::Error, severity: HostDiagnosticSeverity::Error,
message: "fixture rejected Worker create".to_string(), message: "fixture rejected Worker create".to_string(),
@@ -44981,6 +45012,7 @@ mod tests {
kind: "ticket_backend_error".to_string(), kind: "ticket_backend_error".to_string(),
message: "sqlite error: FOREIGN KEY constraint failed".to_string(), message: "sqlite error: FOREIGN KEY constraint failed".to_string(),
diagnostics: vec![RuntimeDiagnostic { diagnostics: vec![RuntimeDiagnostic {
startup: None,
code: "ticket_backend_error".to_string(), code: "ticket_backend_error".to_string(),
severity: HostDiagnosticSeverity::Error, severity: HostDiagnosticSeverity::Error,
message: "FOREIGN KEY constraint failed".to_string(), message: "FOREIGN KEY constraint failed".to_string(),
@@ -51633,6 +51665,7 @@ mod tests {
#[test] #[test]
fn unrecoverable_pending_workspace_restore_is_typed_for_replacement() { fn unrecoverable_pending_workspace_restore_is_typed_for_replacement() {
let diagnostics = [RuntimeDiagnostic { let diagnostics = [RuntimeDiagnostic {
startup: None,
code: "embedded_worker_execution_rejected".to_string(), code: "embedded_worker_execution_rejected".to_string(),
severity: HostDiagnosticSeverity::Error, severity: HostDiagnosticSeverity::Error,
message: "Restore Errored: pending Workspace Worker restore requires operation-owned launch material; generic restore must not reconstruct it from current Workspace config".to_string(), message: "Restore Errored: pending Workspace Worker restore requires operation-owned launch material; generic restore must not reconstruct it from current Workspace config".to_string(),
@@ -51644,6 +51677,7 @@ mod tests {
#[test] #[test]
fn workdir_runtime_miss_uses_exact_typed_code() { fn workdir_runtime_miss_uses_exact_typed_code() {
let typed_not_found = [RuntimeDiagnostic { let typed_not_found = [RuntimeDiagnostic {
startup: None,
code: "working_directory_not_found".to_string(), code: "working_directory_not_found".to_string(),
severity: HostDiagnosticSeverity::Warning, severity: HostDiagnosticSeverity::Warning,
message: "missing".to_string(), message: "missing".to_string(),
@@ -51655,6 +51689,7 @@ mod tests {
assert!(workdir_runtime_miss_is_not_found(&typed_not_found)); assert!(workdir_runtime_miss_is_not_found(&typed_not_found));
let unrelated = [RuntimeDiagnostic { let unrelated = [RuntimeDiagnostic {
startup: None,
code: "some_other_not_found".to_string(), code: "some_other_not_found".to_string(),
severity: HostDiagnosticSeverity::Warning, severity: HostDiagnosticSeverity::Warning,
message: "not a typed workdir miss".to_string(), message: "not a typed workdir miss".to_string(),
@@ -63358,6 +63393,7 @@ mod tests {
&api, &api,
deleted, deleted,
&[RuntimeDiagnostic { &[RuntimeDiagnostic {
startup: None,
code: "working_directory_not_found".to_string(), code: "working_directory_not_found".to_string(),
severity: HostDiagnosticSeverity::Warning, severity: HostDiagnosticSeverity::Warning,
message: "missing".to_string(), message: "missing".to_string(),
@@ -63384,6 +63420,7 @@ mod tests {
&api, &api,
unknown, unknown,
&[RuntimeDiagnostic { &[RuntimeDiagnostic {
startup: None,
code: "runtime_unavailable".to_string(), code: "runtime_unavailable".to_string(),
severity: HostDiagnosticSeverity::Warning, severity: HostDiagnosticSeverity::Warning,
message: "temporarily unavailable".to_string(), message: "temporarily unavailable".to_string(),
@@ -65080,6 +65117,7 @@ mod tests {
InternalWorkerOperationState::Rejected, InternalWorkerOperationState::Rejected,
None, None,
vec![RuntimeDiagnostic { vec![RuntimeDiagnostic {
startup: None,
code: "working_directory_not_found".into(), code: "working_directory_not_found".into(),
severity: HostDiagnosticSeverity::Warning, severity: HostDiagnosticSeverity::Warning,
message: "authoritatively absent".into(), message: "authoritatively absent".into(),
@@ -65131,6 +65169,7 @@ mod tests {
InternalWorkerOperationState::Rejected, InternalWorkerOperationState::Rejected,
None, None,
vec![RuntimeDiagnostic { vec![RuntimeDiagnostic {
startup: None,
code: "working_directory_cleanup_resource_busy".into(), code: "working_directory_cleanup_resource_busy".into(),
severity: HostDiagnosticSeverity::Warning, severity: HostDiagnosticSeverity::Warning,
message: "busy".into(), message: "busy".into(),
@@ -65360,6 +65399,7 @@ mod tests {
let (missing_operation, _) = reserve_removal_fixture(&api, "provider-not-found"); let (missing_operation, _) = reserve_removal_fixture(&api, "provider-not-found");
let not_found = RuntimeDiagnostic { let not_found = RuntimeDiagnostic {
startup: None,
code: "working_directory_not_found".to_string(), code: "working_directory_not_found".to_string(),
severity: HostDiagnosticSeverity::Error, severity: HostDiagnosticSeverity::Error,
message: "missing".to_string(), message: "missing".to_string(),
@@ -65395,6 +65435,7 @@ mod tests {
InternalWorkerOperationState::Accepted, InternalWorkerOperationState::Accepted,
None, None,
vec![RuntimeDiagnostic { vec![RuntimeDiagnostic {
startup: None,
code: "working_directory_provider_timeout".to_string(), code: "working_directory_provider_timeout".to_string(),
severity: HostDiagnosticSeverity::Error, severity: HostDiagnosticSeverity::Error,
message: "timeout".to_string(), message: "timeout".to_string(),
@@ -65511,6 +65552,7 @@ mod tests {
InternalWorkerOperationState::Unsupported, InternalWorkerOperationState::Unsupported,
None, None,
vec![RuntimeDiagnostic { vec![RuntimeDiagnostic {
startup: None,
code: "working_directory_unsupported".to_string(), code: "working_directory_unsupported".to_string(),
severity: HostDiagnosticSeverity::Error, severity: HostDiagnosticSeverity::Error,
message: "unsupported".to_string(), message: "unsupported".to_string(),
@@ -65571,6 +65613,7 @@ mod tests {
InternalWorkerOperationState::Rejected, InternalWorkerOperationState::Rejected,
None, None,
vec![RuntimeDiagnostic { vec![RuntimeDiagnostic {
startup: None,
code: code.into(), code: code.into(),
severity: HostDiagnosticSeverity::Error, severity: HostDiagnosticSeverity::Error,
message: "secret /private/path".into(), message: "secret /private/path".into(),
@@ -65602,6 +65645,7 @@ mod tests {
InternalWorkerOperationState::Rejected, InternalWorkerOperationState::Rejected,
None, None,
vec![RuntimeDiagnostic { vec![RuntimeDiagnostic {
startup: None,
code: "working_directory_cleanup_mount_present".into(), code: "working_directory_cleanup_mount_present".into(),
severity: HostDiagnosticSeverity::Error, severity: HostDiagnosticSeverity::Error,
message: "/private/mount credentials must not escape".into(), message: "/private/mount credentials must not escape".into(),
@@ -65752,6 +65796,7 @@ mod tests {
InternalWorkerOperationState::Rejected, InternalWorkerOperationState::Rejected,
None, None,
vec![RuntimeDiagnostic { vec![RuntimeDiagnostic {
startup: None,
code: "working_directory_cleanup_permission_denied".into(), code: "working_directory_cleanup_permission_denied".into(),
severity: HostDiagnosticSeverity::Error, severity: HostDiagnosticSeverity::Error,
message: "permission denied /private/path".into(), message: "permission denied /private/path".into(),
@@ -65938,6 +65983,7 @@ mod tests {
state: InternalWorkerOperationState::Rejected, state: InternalWorkerOperationState::Rejected,
working_directory: None, working_directory: None,
diagnostics: vec![RuntimeDiagnostic { diagnostics: vec![RuntimeDiagnostic {
startup: None,
code: "working_directory_not_found".to_string(), code: "working_directory_not_found".to_string(),
severity: HostDiagnosticSeverity::Error, severity: HostDiagnosticSeverity::Error,
message: "missing".to_string(), message: "missing".to_string(),
@@ -65949,6 +65995,7 @@ mod tests {
state: InternalWorkerOperationState::Rejected, state: InternalWorkerOperationState::Rejected,
working_directory: None, working_directory: None,
diagnostics: vec![RuntimeDiagnostic { diagnostics: vec![RuntimeDiagnostic {
startup: None,
code: "working_directory_provider_timeout".to_string(), code: "working_directory_provider_timeout".to_string(),
severity: HostDiagnosticSeverity::Error, severity: HostDiagnosticSeverity::Error,
message: "timeout".to_string(), message: "timeout".to_string(),
@@ -67641,11 +67688,13 @@ mod tests {
fn provider_rejection_classification_prefers_stable_error_diagnostic() { fn provider_rejection_classification_prefers_stable_error_diagnostic() {
let diagnostics = vec![ let diagnostics = vec![
RuntimeDiagnostic { RuntimeDiagnostic {
startup: None,
code: "runtime_capacity_warning".to_string(), code: "runtime_capacity_warning".to_string(),
severity: HostDiagnosticSeverity::Warning, severity: HostDiagnosticSeverity::Warning,
message: "capacity is low".to_string(), message: "capacity is low".to_string(),
}, },
RuntimeDiagnostic { RuntimeDiagnostic {
startup: None,
code: "runtime_capacity_unavailable".to_string(), code: "runtime_capacity_unavailable".to_string(),
severity: HostDiagnosticSeverity::Error, severity: HostDiagnosticSeverity::Error,
message: "capacity is exhausted".to_string(), message: "capacity is exhausted".to_string(),
@@ -67705,6 +67754,7 @@ mod tests {
}) })
.unwrap(); .unwrap();
let diagnostics = vec![RuntimeDiagnostic { let diagnostics = vec![RuntimeDiagnostic {
startup: None,
code: "runtime_capacity_unavailable".to_string(), code: "runtime_capacity_unavailable".to_string(),
severity: HostDiagnosticSeverity::Error, severity: HostDiagnosticSeverity::Error,
message: "capacity is exhausted".to_string(), message: "capacity is exhausted".to_string(),
@@ -0,0 +1,369 @@
//! Focused T840 connection: real Subject policy -> signed Session POST -> real
//! ProfileRuntimeWorkerFactory startup -> RuntimeHTTP/Host diagnostic -> saved
//! Worker metadata. Only the unused external model is replaced. No restore,
//! operation replay, singleton-release, or Browser rendering contract is asserted.
use super::*;
use protocol::session_failure::{
SessionPrincipal, SessionRegistrationCause, session_registration_attachment,
};
use session_store::WorkerMetadataStore as _;
use std::sync::atomic::{AtomicUsize, Ordering};
use worker_runtime::worker_backend::RuntimeWorkerFactory as _;
#[derive(Clone, Default)]
struct UnusedModel(Arc<AtomicUsize>);
#[async_trait::async_trait]
impl agen::llm_client::LlmClient for UnusedModel {
fn clone_boxed(&self) -> Box<dyn agen::llm_client::LlmClient> {
Box::new(self.clone())
}
async fn stream(
&self,
_: agen::llm_client::Request,
) -> std::result::Result<agen::llm_client::client::ResponseStream, agen::llm_client::ClientError>
{
self.0.fetch_add(1, Ordering::SeqCst);
panic!("Session registration does not require a model turn")
}
}
struct RegistrationFixture {
connected: ConnectedWorkerHarness,
subject: String,
callbacks: Arc<Mutex<Vec<(StatusCode, Value)>>>,
model: UnusedModel,
}
impl RegistrationFixture {
async fn new(revoke_on_registration: bool) -> Self {
// Armed only after real Subject creation with its normal default policy.
// The callback is a deterministic synchronization point, not a fake Host
// rejection: the original signed request and real handler remain intact.
let revoke = Arc::new(Mutex::new(None::<(WorkspaceApi, String, PathBuf)>));
let hook = revoke.clone();
let callbacks = Arc::new(Mutex::new(Vec::new()));
let observed = callbacks.clone();
let model = UnusedModel::default();
let connected = ConnectedWorkerHarness::start_with_layer(model.clone(), move |router| {
router.layer(axum::middleware::from_fn(
move |request: axum::extract::Request, next: axum::middleware::Next| {
let hook = hook.clone();
let observed = observed.clone();
async move {
if request.method() != axum::http::Method::POST
|| request.uri().path()
!= format!("/api/w/{TEST_WORKSPACE_ID}/subjektiv/sessions")
{
return next.run(request).await;
}
assert!(request.headers().contains_key(
worker_runtime::auth::RUNTIME_REQUEST_SOURCE_PROOF_HEADER
));
let armed = hook.lock().unwrap().take();
let pin_root = armed.as_ref().map(|(_, _, root)| root.clone());
if let Some((api, subject, _)) = armed {
let store = open_subjektiv_store(&api).unwrap();
let policy = store.subject_permissions(&subject).unwrap().unwrap();
assert!(
policy
.execution
.iter()
.any(|p| p.operation == "session.record")
);
store
.set_subject_permissions(
&subject,
&SubjectPermissionsUpdate {
operation_id: "session-registration-revoke-at-callback"
.into(),
expected_change_id: Some(policy.change_id),
execution: policy
.execution
.into_iter()
.filter(|p| p.operation != "session.record")
.collect(),
delegation: policy.delegation,
},
)
.unwrap();
}
let (parts, body) = request.into_parts();
let bytes = to_bytes(body, 2 * 1024 * 1024).await.unwrap();
let input: Value = serde_json::from_slice(&bytes).unwrap();
if let Some(root) = pin_root {
let request: server_api::SubjektivRecordSessionRequest = serde_json::from_slice(&bytes).unwrap();
let session = request.session_id.parse().unwrap();
let retention = worker_runtime::retention::runtime_session_retention(&root);
retention.pin(session, session_store::retention::SessionRetentionPin(uuid::Uuid::now_v7()), || {
let entries = std::fs::read_dir(root.join("workers"))?;
for entry in entries {
let entry = entry?;
let name = format!("worker-runtime-{}", entry.file_name().to_string_lossy());
let store = session_store::WorkerAggregateStore::open_read_only(&entry.path(), &name)
.map_err(|error| session_store::StoreError::Io(std::io::Error::other(error)))?;
if store.read_by_name(&name).unwrap().and_then(|m| m.active).is_some_and(|a| a.session_id == session) { return Ok(true); }
}
Ok(false)
}).unwrap();
}
let response = next
.run(Request::from_parts(parts, Body::from(bytes)))
.await;
observed.lock().unwrap().push((response.status(), input));
response
}
},
))
})
.await;
let subject = open_subjektiv_store(&connected.api)
.unwrap()
.create_subject(
crate::subjektiv::SubjectRole::new("session-registration-connected").unwrap(),
)
.unwrap()
.id;
let initial = open_subjektiv_store(&connected.api)
.unwrap()
.subject_permissions(&subject)
.unwrap()
.unwrap();
assert!(
initial
.execution
.iter()
.any(|p| p.operation == "session.record")
);
if revoke_on_registration {
*revoke.lock().unwrap() = Some((
connected.api.clone(),
subject.clone(),
connected.runtime_root.clone(),
));
}
Self {
connected,
subject,
callbacks,
model,
}
}
fn request(&self) -> CreateWorkspaceWorkerRequest {
CreateWorkspaceWorkerRequest {
runtime_id: CONNECTED_RUNTIME.into(),
display_name: "Connected Session registration".into(),
singleton_key: None,
profile: Some("builtin:companion".into()),
ticket_assignment: None,
initial_submit: vec![],
workdir_attachments: vec![],
feature_connections: server_api::WorkspaceWorkerFeatureConnectionsRequest {
subjektiv: Some(server_api::SubjektivWorkerConnectionRequest {
subject_id: self.subject.clone(),
}),
},
control_operation_id: None,
execution_grant: None,
}
}
fn assert_callback(&self, expected: StatusCode) -> String {
let callbacks = self.callbacks.lock().unwrap();
assert!(!callbacks.is_empty(), "real signed startup callback");
assert!(callbacks.iter().all(|(status, body)| *status == expected
&& body["session_id"] == callbacks[0].1["session_id"]));
let (status, input) = &callbacks[0];
assert_eq!(*status, expected);
assert_eq!(input["create_if_missing"], true);
assert_eq!(self.model.0.load(Ordering::SeqCst), 0);
input["session_id"].as_str().unwrap().to_owned()
}
}
// This deliberately inspects only persisted serde shape, not rendered error text
// or an error-contract dependency. The expected value is a typed live attachment.
fn contains_saved_value(saved: &Value, expected: &Value) -> bool {
saved == expected
|| match saved {
Value::Object(fields) => fields
.values()
.any(|value| contains_saved_value(value, expected)),
Value::Array(values) => values
.iter()
.any(|value| contains_saved_value(value, expected)),
_ => false,
}
}
fn read_metadata(root: &std::path::Path, worker: &str) -> Value {
// WorkerAggregateStore owns metadata.json; worker.json is the Runtime
// catalog. Open a new file handle on each read.
let path = root.join("workers").join(worker).join("metadata.json");
serde_json::from_reader(std::fs::File::open(path).unwrap()).unwrap()
}
#[tokio::test(flavor = "multi_thread", worker_threads = 6)]
async fn real_session_record_denial_survives_runtime_host_and_metadata_reopen() {
let fixture = RegistrationFixture::new(true).await;
// The real create preflight sees normal defaults. Revocation happens only
// after the actual factory has issued its signed Session-registration POST.
let error = scoped_create_workspace_worker(
State(fixture.connected.api.clone()),
AxumPath(ScopedWorkspacePath {
workspace_id: TEST_WORKSPACE_ID.into(),
}),
HeaderMap::new(),
Json(fixture.request()),
)
.await
.unwrap_err();
fixture.assert_callback(StatusCode::FORBIDDEN);
let store = open_subjektiv_store(&fixture.connected.api).unwrap();
assert!(
store
.subject_session_attributions(&fixture.subject)
.unwrap()
.is_empty()
);
assert!(
!store
.subject_permissions(&fixture.subject)
.unwrap()
.unwrap()
.execution
.iter()
.any(|p| p.operation == "session.record")
);
let failures = fixture.connected.runtime_startup_failures.lock().unwrap();
assert_eq!(
failures.len(),
1,
"actual RuntimeHTTP startup error envelope"
);
let wire = &failures[0];
let expected_primary = session_registration_attachment(SessionRegistrationCause::denied(
SessionPrincipal::Subject,
))
.unwrap();
assert_eq!(wire.primary, expected_primary);
assert_eq!(
wire.persistence,
Some(protocol::startup_failure::StartupPersistenceOutcome::Persisted)
);
let host = error
.diagnostics
.iter()
.filter_map(|d| d.startup.as_ref())
.find(|startup| startup.primary == wire.primary)
.expect("RemoteWorkerRuntime must preserve the typed startup primary in Host diagnostics");
assert_eq!(host.primary, wire.primary);
assert!(
host.secondary.iter().any(|failure| matches!(
failure,
protocol::startup_failure::StartupSecondaryFailure::Persistence {
phase: protocol::startup_failure::StartupFailurePhase::RuntimeRecordDelete,
..
}
)),
"a real Session retention pin rejects outer Runtime deletion without hiding the primary"
);
let expected = serde_json::to_value(&wire.primary).unwrap();
assert!(
expected.is_string(),
"the shared attachment contains a codec envelope"
);
// A fresh fixture has exactly one attempted Worker. Discover its canonical
// saved aggregate, without making any assumptions about retained ownership
// or public catalog visibility after a failed create.
let workers = std::fs::read_dir(fixture.connected.runtime_root.join("workers"))
.unwrap()
.map(|entry| entry.unwrap().file_name().into_string().unwrap())
.collect::<Vec<_>>();
assert_eq!(
workers.len(),
1,
"factory attempt must have real saved metadata"
);
let saved = read_metadata(&fixture.connected.runtime_root, &workers[0]);
assert!(
contains_saved_value(&saved, &expected),
"saved metadata must retain the original typed Session registration denial"
);
let reopened = read_metadata(&fixture.connected.runtime_root, &workers[0]);
assert_eq!(
reopened, saved,
"read-only reopen, not a startup operation retry"
);
assert!(contains_saved_value(&reopened, &expected));
}
#[tokio::test(flavor = "multi_thread", worker_threads = 6)]
async fn permitted_session_registration_reaches_real_factory_and_saved_confirmation() {
let fixture = RegistrationFixture::new(false).await;
let initial = open_subjektiv_store(&fixture.connected.api)
.unwrap()
.subject_permissions(&fixture.subject)
.unwrap()
.unwrap();
let Json(created) = scoped_create_workspace_worker(
State(fixture.connected.api.clone()),
AxumPath(ScopedWorkspacePath {
workspace_id: TEST_WORKSPACE_ID.into(),
}),
HeaderMap::new(),
Json(fixture.request()),
)
.await
.unwrap();
let session_id = fixture.assert_callback(StatusCode::OK);
let store = open_subjektiv_store(&fixture.connected.api).unwrap();
assert_eq!(
store
.subject_permissions(&fixture.subject)
.unwrap()
.unwrap(),
initial
);
let attributions = store
.subject_session_attributions(&fixture.subject)
.unwrap();
assert_eq!(attributions.len(), 1);
assert_eq!(attributions[0].session_id, session_id);
assert_eq!(attributions[0].worker_id, created.worker_id);
assert_eq!(attributions[0].runtime_id, CONNECTED_RUNTIME);
assert!(
fixture
.connected
.runtime_startup_failures
.lock()
.unwrap()
.is_empty()
);
let name = format!("worker-runtime-{}", created.worker_id);
let metadata = session_store::WorkerAggregateStore::open_read_only(
&fixture
.connected
.runtime_root
.join("workers")
.join(&created.worker_id),
&name,
)
.unwrap();
let saved = metadata.read_by_name(&name).unwrap().unwrap();
assert!(matches!(
saved.subjektiv_session_attribution,
Some(session_store::SubjektivSessionAttributionState::Confirmed { .. })
));
let source = RuntimeWorkerRef::new(created.runtime_id, created.worker_id);
fixture
.connected
.factory
.retained_session_snapshot(&worker_runtime::identity::WorkerRef::try_from(&source).unwrap())
.expect("real factory retained the permitted Worker Session");
}
@@ -9,6 +9,8 @@ use super::*;
mod chat_delegation_connected; mod chat_delegation_connected;
#[path = "chat_tests.rs"] #[path = "chat_tests.rs"]
mod chat_tests; mod chat_tests;
#[path = "session_registration_connected_tests.rs"]
mod session_registration_connected;
#[path = "subject_body_cleanup_connected_tests.rs"] #[path = "subject_body_cleanup_connected_tests.rs"]
mod subject_body_cleanup_connected; mod subject_body_cleanup_connected;
#[path = "subject_body_connected_tests.rs"] #[path = "subject_body_connected_tests.rs"]
@@ -2737,6 +2739,7 @@ pub(super) struct ConnectedWorkerHarness {
materializer: worker_runtime::working_directory::RuntimeGitMaterializer, materializer: worker_runtime::working_directory::RuntimeGitMaterializer,
authorizations: Arc<Mutex<Vec<(Value, StatusCode)>>>, authorizations: Arc<Mutex<Vec<(Value, StatusCode)>>>,
runtime_errors: Arc<Mutex<Vec<String>>>, runtime_errors: Arc<Mutex<Vec<String>>>,
runtime_startup_failures: Arc<Mutex<Vec<runtime_api::WorkerStartupFailure>>>,
_backend_server: AuthorizationCallbackServer, _backend_server: AuthorizationCallbackServer,
_runtime_server: ConnectedRuntimeServer, _runtime_server: ConnectedRuntimeServer,
_dir: tempfile::TempDir, _dir: tempfile::TempDir,
@@ -2942,6 +2945,8 @@ impl ConnectedWorkerHarness {
.unwrap(); .unwrap();
let runtime_errors = Arc::new(Mutex::new(Vec::<String>::new())); let runtime_errors = Arc::new(Mutex::new(Vec::<String>::new()));
let captured_errors = runtime_errors.clone(); let captured_errors = runtime_errors.clone();
let runtime_startup_failures = Arc::new(Mutex::new(Vec::new()));
let captured_startup_failures = runtime_startup_failures.clone();
let runtime_server = ConnectedRuntimeServer::start( let runtime_server = ConnectedRuntimeServer::start(
runtime_listener, runtime_listener,
worker_runtime::http_server::runtime_http_router_with_workspace_auth( worker_runtime::http_server::runtime_http_router_with_workspace_auth(
@@ -2955,6 +2960,7 @@ impl ConnectedWorkerHarness {
.layer(axum::middleware::from_fn( .layer(axum::middleware::from_fn(
move |request: axum::extract::Request, next: axum::middleware::Next| { move |request: axum::extract::Request, next: axum::middleware::Next| {
let errors = captured_errors.clone(); let errors = captured_errors.clone();
let startup_failures = captured_startup_failures.clone();
async move { async move {
let response = next.run(request).await; let response = next.run(request).await;
if !response.status().is_client_error() if !response.status().is_client_error()
@@ -2965,6 +2971,13 @@ impl ConnectedWorkerHarness {
let (parts, body) = response.into_parts(); let (parts, body) = response.into_parts();
let bytes = to_bytes(body, 2 * 1024 * 1024).await.unwrap(); let bytes = to_bytes(body, 2 * 1024 * 1024).await.unwrap();
let body: Value = serde_json::from_slice(&bytes).unwrap(); let body: Value = serde_json::from_slice(&bytes).unwrap();
if let Some(startup) = body["error"].get("startup").filter(|v| !v.is_null())
{
startup_failures.lock().unwrap().push(
serde_json::from_value(startup.clone())
.expect("RuntimeHTTP structured startup failure"),
);
}
errors.lock().unwrap().push( errors.lock().unwrap().push(
body["error"]["message"] body["error"]["message"]
.as_str() .as_str()
@@ -3045,6 +3058,7 @@ impl ConnectedWorkerHarness {
materializer, materializer,
authorizations, authorizations,
runtime_errors, runtime_errors,
runtime_startup_failures,
_backend_server: backend_server, _backend_server: backend_server,
_runtime_server: runtime_server, _runtime_server: runtime_server,
_dir: dir, _dir: dir,