diff --git a/crates/runtime-api/src/lib.rs b/crates/runtime-api/src/lib.rs index ab2a611af..757ab0ee0 100644 --- a/crates/runtime-api/src/lib.rs +++ b/crates/runtime-api/src/lib.rs @@ -402,6 +402,8 @@ pub struct WorkingDirectoryStatus { #[serde(rename_all = "snake_case")] pub enum WorkerInputKind { User, + /// Agent input accepted only while Idle; never enters the human Submit queue. + UserIfIdle, Notify, Compact, ListRewindTargets, diff --git a/crates/worker-runtime/src/http_server.rs b/crates/worker-runtime/src/http_server.rs index 1935f023e..9c1a4f7fa 100644 --- a/crates/worker-runtime/src/http_server.rs +++ b/crates/worker-runtime/src/http_server.rs @@ -3490,14 +3490,31 @@ mod tests { conflicting_request.create_fingerprint = "different-fingerprint".to_string(); let generated = client.create_worker(generated_request).await.unwrap(); assert_eq!(generated.worker.worker_id.to_string(), generated_worker_id); - let generated_input: runtime_api::WorkerInput = serde_json::from_value( - serde_json::to_value(WorkerInput::user("generated client input")).unwrap(), - ) - .unwrap(); - client - .send_worker_input(generated_worker_id.clone(), generated_input) - .await - .unwrap(); + for kind in [ + crate::interaction::WorkerInputKind::User, + crate::interaction::WorkerInputKind::UserIfIdle, + ] { + let mut input = WorkerInput::user("generated client input"); + input.kind = kind.clone(); + input.submission_request_id = Some(format!("generated-{kind:?}")); + // The remote adapter performs this same conversion before sending. + // Both the generated API contract and the HTTP receiver must retain + // Idle-only admission rather than silently downgrading it to User. + let generated_input: runtime_api::WorkerInput = + serde_json::from_value(serde_json::to_value(&input).unwrap()).unwrap(); + assert_eq!( + serde_json::to_value(&generated_input).unwrap(), + serde_json::to_value(&input).unwrap() + ); + let response = client + .send_worker_input(generated_worker_id.clone(), generated_input) + .await + .unwrap(); + assert_eq!( + Some(response.ack.submission.unwrap().submission_request_id), + input.submission_request_id + ); + } client .stop_worker( generated_worker_id.clone(), diff --git a/crates/workspace-server/src/hosts.rs b/crates/workspace-server/src/hosts.rs index 40ab2b09b..aa903a20f 100644 --- a/crates/workspace-server/src/hosts.rs +++ b/crates/workspace-server/src/hosts.rs @@ -6341,6 +6341,28 @@ mod tests { assert_eq!(delivered.content, archive.content); } + #[test] + fn runtime_input_contract_preserves_every_input_kind() { + for kind in [ + EmbeddedWorkerInputKind::User, + EmbeddedWorkerInputKind::UserIfIdle, + EmbeddedWorkerInputKind::Notify, + EmbeddedWorkerInputKind::Compact, + EmbeddedWorkerInputKind::ListRewindTargets, + EmbeddedWorkerInputKind::RegisterPeer, + ] { + let input = EmbeddedWorkerInput { + kind, + content: "contract test".to_string(), + submission_request_id: Some("contract-request".to_string()), + segments: Some(vec![protocol::Segment::text("contract test")]), + }; + let wire: runtime_api::WorkerInput = runtime_contract_convert(input.clone()).unwrap(); + let decoded: EmbeddedWorkerInput = runtime_contract_convert(wire).unwrap(); + assert_eq!(decoded, input); + } + } + #[test] fn remote_profile_source_uses_workspace_config_archive_reference() { let request = embedded_spawn_request();