1use axum::{
40 extract::{
41 ws::{Message, WebSocket, WebSocketUpgrade},
42 Path, State,
43 },
44 http::{HeaderMap, StatusCode},
45 response::{IntoResponse, Response},
46 Json,
47};
48use futures_util::{sink::SinkExt, stream::StreamExt};
49use mlua_swarm::{AgentProviderManifest, Operator, SeniorBridge, SessionId, SpawnHook};
50use serde::{Deserialize, Serialize};
51use serde_json::json;
52use std::sync::Arc;
53use tokio::sync::{mpsc, Mutex};
54
55use super::protocol::{ClientMsg, PendingReply, ServerMsg};
56use super::session::WSOperatorSession;
57use crate::AppState;
58
59pub struct OperatorSessionEntry {
65 pub sid: SessionId,
67 pub token: String,
69 pub roles: Vec<String>,
71 pub capability_manifest: Option<AgentProviderManifest>,
73 pub joined_at_secs: u64,
78 pub ws_session: Mutex<Option<Arc<WSOperatorSession>>>,
81}
82
83#[derive(Debug, Deserialize, Default)]
87pub struct OperatorsCreateReq {
88 #[serde(default)]
90 pub roles: Vec<String>,
91 #[serde(default)]
93 pub capability_manifest: Option<AgentProviderManifest>,
94}
95
96#[derive(Debug, Serialize)]
98pub struct OperatorsCreateResp {
99 pub sid: SessionId,
102 pub token: String,
104 pub roles: Vec<String>,
106}
107
108pub async fn operators_create(
118 State(state): State<AppState>,
119 Json(req): Json<OperatorsCreateReq>,
120) -> Response {
121 let roles = req.roles;
122 let capability_manifest = req.capability_manifest;
123 let sid = SessionId::new();
130 let token = mlua_swarm::types::secure_hex(5);
131
132 {
133 let mut map = state.roles_to_sid.lock().await;
134 let conflicts: Vec<String> = roles
135 .iter()
136 .filter(|r| map.contains_key(r.as_str()))
137 .cloned()
138 .collect();
139 if !conflicts.is_empty() {
140 let conflicts_detail: Vec<serde_json::Value> = conflicts
147 .iter()
148 .map(|r| {
149 let holder = map.get(r.as_str()).map(|sid| sid.to_string());
150 json!({ "role": r, "sid": holder })
151 })
152 .collect();
153 return (
154 StatusCode::CONFLICT,
155 Json(json!({
156 "error": "roles conflict",
157 "conflicts": conflicts,
158 "conflicts_detail": conflicts_detail,
159 })),
160 )
161 .into_response();
162 }
163 for r in &roles {
164 map.insert(r.clone(), sid.clone());
165 }
166 }
167
168 let joined_at_secs = std::time::SystemTime::now()
169 .duration_since(std::time::UNIX_EPOCH)
170 .map(|d| d.as_secs())
171 .unwrap_or(0);
172 let entry = Arc::new(OperatorSessionEntry {
173 sid: sid.clone(),
174 token: token.clone(),
175 roles: roles.clone(),
176 capability_manifest,
177 joined_at_secs,
178 ws_session: Mutex::new(None),
179 });
180 state
181 .operator_sessions
182 .lock()
183 .await
184 .insert(sid.clone(), entry);
185
186 (
187 StatusCode::OK,
188 Json(OperatorsCreateResp { sid, token, roles }),
189 )
190 .into_response()
191}
192
193fn extract_bearer_token_required(headers: &HeaderMap) -> Result<String, Box<Response>> {
199 let token = headers
200 .get(axum::http::header::AUTHORIZATION)
201 .and_then(|v| v.to_str().ok())
202 .and_then(|s| s.strip_prefix("Bearer "))
203 .map(|s| s.trim().to_string())
204 .filter(|s| !s.is_empty());
205 token.ok_or_else(|| {
206 Box::new((StatusCode::UNAUTHORIZED, "missing or empty Bearer token").into_response())
207 })
208}
209
210pub async fn operators_ws_connect(
216 State(state): State<AppState>,
217 Path(sid): Path<String>,
218 headers: HeaderMap,
219 ws: WebSocketUpgrade,
220) -> Response {
221 let bearer = match extract_bearer_token_required(&headers) {
222 Ok(t) => t,
223 Err(resp) => return *resp,
224 };
225 let Ok(sid) = SessionId::parse(sid) else {
227 return (StatusCode::NOT_FOUND, "unknown sid").into_response();
228 };
229
230 let entry = {
231 let map = state.operator_sessions.lock().await;
232 map.get(&sid).cloned()
233 };
234 let entry = match entry {
235 Some(e) => e,
236 None => return (StatusCode::NOT_FOUND, "unknown sid").into_response(),
237 };
238 if !mlua_swarm::types::ct_eq(entry.token.as_bytes(), bearer.as_bytes()) {
239 return (StatusCode::UNAUTHORIZED, "token mismatch").into_response();
240 }
241
242 ws.on_upgrade(move |socket| handle_operator_socket(socket, state, entry))
243}
244
245async fn handle_operator_socket(
249 socket: WebSocket,
250 state: AppState,
251 entry: Arc<OperatorSessionEntry>,
252) {
253 let (tx, mut rx) = mpsc::unbounded_channel::<ServerMsg>();
254
255 let existing_ws = entry.ws_session.lock().await.clone();
256 let session = match existing_ws {
257 Some(ws_session) => {
258 ws_session.replace_tx(tx.clone()).await;
260 ws_session
261 }
262 None => {
263 let ws_session = Arc::new(WSOperatorSession::new_with_base_url(
264 entry.sid.clone(),
265 tx.clone(),
266 state.base_url.clone(),
267 ));
268 state
269 .engine
270 .register_senior_bridge(
271 entry.sid.clone(),
272 ws_session.clone() as Arc<dyn SeniorBridge>,
273 )
274 .await;
275 state
276 .engine
277 .register_spawn_hook(entry.sid.clone(), ws_session.clone() as Arc<dyn SpawnHook>)
278 .await;
279 state
280 .engine
281 .register_operator(entry.sid.clone(), ws_session.clone() as Arc<dyn Operator>)
282 .await;
283 if let Some(factory) = &state.ws_operator_factory {
284 factory
285 .register_operator(entry.sid.clone(), ws_session.clone() as Arc<dyn Operator>);
286 }
287 for role in &entry.roles {
292 if let Some(factory) = &state.ws_operator_factory {
293 factory
294 .register_operator(role.clone(), ws_session.clone() as Arc<dyn Operator>);
295 }
296 state
297 .engine
298 .register_operator(role.clone(), ws_session.clone() as Arc<dyn Operator>)
299 .await;
300 }
301 *entry.ws_session.lock().await = Some(ws_session.clone());
302 ws_session
303 }
304 };
305
306 let (mut ws_sink, mut ws_stream) = socket.split();
307
308 let write_task = tokio::spawn(async move {
310 while let Some(msg) = rx.recv().await {
311 let txt = match serde_json::to_string(&msg) {
312 Ok(s) => s,
313 Err(_) => continue,
314 };
315 if ws_sink.send(Message::Text(txt)).await.is_err() {
316 break;
317 }
318 }
319 let _ = ws_sink.close().await;
320 });
321
322 let session_for_read = session.clone();
324 let read_result: Result<(), String> = async {
325 while let Some(item) = ws_stream.next().await {
326 match item {
327 Ok(Message::Text(t)) => {
328 let parsed: ClientMsg = match serde_json::from_str(&t) {
329 Ok(p) => p,
330 Err(_) => continue,
331 };
332 match parsed {
333 ClientMsg::Answer { req_id, value } => {
334 session_for_read
335 .resolve_pending(&req_id, PendingReply::Answer(value))
336 .await;
337 }
338 ClientMsg::HookAck { req_id, ok, reason } => {
339 session_for_read
340 .resolve_pending(&req_id, PendingReply::HookAck { ok, reason })
341 .await;
342 }
343 ClientMsg::SpawnAck {
344 req_id,
345 value,
346 ok,
347 error,
348 stats,
349 } => {
350 session_for_read
351 .resolve_pending(
352 &req_id,
353 PendingReply::SpawnAck {
354 value,
355 ok,
356 error,
357 stats,
358 },
359 )
360 .await;
361 }
362 ClientMsg::SpawnHalt {
363 req_id,
364 value,
365 reason,
366 } => {
367 session_for_read
368 .resolve_pending(&req_id, PendingReply::SpawnHalt { value, reason })
369 .await;
370 }
371 }
372 }
373 Ok(Message::Ping(_)) | Ok(Message::Pong(_)) => {}
374 Ok(Message::Close(_)) | Err(_) => break,
375 _ => {}
376 }
377 }
378 Ok(())
379 }
380 .await;
381
382 session.clear_tx_if(&tx).await;
385 write_task.abort();
386 let _ = read_result;
387}
388
389async fn teardown_operator_session(
399 state: &AppState,
400 sid: &SessionId,
401 entry: &Arc<OperatorSessionEntry>,
402) {
403 state.engine.unregister_senior_bridge(sid.as_str()).await;
404 state.engine.unregister_spawn_hook(sid.as_str()).await;
405 state.engine.unregister_operator(sid.as_str()).await;
406 if let Some(factory) = &state.ws_operator_factory {
407 factory.unregister_operator(sid.as_str());
408 }
409 for role in &entry.roles {
410 state.engine.unregister_operator(role).await;
411 if let Some(factory) = &state.ws_operator_factory {
412 factory.unregister_operator(role);
413 }
414 }
415
416 if let Some(session) = entry.ws_session.lock().await.take() {
417 session.fail_pending("operator session torn down").await;
424 session.clear_tx().await;
425 }
426
427 state.operator_sessions.lock().await.remove(sid);
428
429 {
430 let mut map = state.roles_to_sid.lock().await;
431 for role in &entry.roles {
432 if map.get(role) == Some(sid) {
433 map.remove(role);
434 }
435 }
436 }
437}
438
439pub async fn operators_delete(
445 State(state): State<AppState>,
446 Path(sid): Path<String>,
447 headers: HeaderMap,
448) -> Response {
449 let bearer = match extract_bearer_token_required(&headers) {
450 Ok(t) => t,
451 Err(resp) => return *resp,
452 };
453 let Ok(sid) = SessionId::parse(sid) else {
454 return (StatusCode::NOT_FOUND, "unknown sid").into_response();
455 };
456
457 let entry = {
458 let map = state.operator_sessions.lock().await;
459 map.get(&sid).cloned()
460 };
461 let entry = match entry {
462 Some(e) => e,
463 None => return (StatusCode::NOT_FOUND, "unknown sid").into_response(),
464 };
465 if !mlua_swarm::types::ct_eq(entry.token.as_bytes(), bearer.as_bytes()) {
466 return (StatusCode::UNAUTHORIZED, "token mismatch").into_response();
467 }
468
469 teardown_operator_session(&state, &sid, &entry).await;
470
471 StatusCode::NO_CONTENT.into_response()
472}
473
474#[derive(Debug, Serialize)]
481pub struct OperatorsListEntry {
482 pub sid: SessionId,
484 pub roles: Vec<String>,
486 pub joined_at_secs: u64,
489 pub connected: bool,
492}
493
494#[derive(Debug, Serialize)]
496pub struct OperatorsListResp {
497 pub operators: Vec<OperatorsListEntry>,
500}
501
502pub async fn operators_list(State(state): State<AppState>) -> Response {
509 let entries: Vec<(SessionId, Arc<OperatorSessionEntry>)> = {
510 let map = state.operator_sessions.lock().await;
511 map.iter().map(|(k, v)| (k.clone(), v.clone())).collect()
512 };
513 let mut operators = Vec::with_capacity(entries.len());
514 for (sid, entry) in entries {
515 let session = entry.ws_session.lock().await.clone();
516 let connected = match session {
517 Some(session) => session.is_connected().await,
518 None => false,
519 };
520 operators.push(OperatorsListEntry {
521 sid,
522 roles: entry.roles.clone(),
523 joined_at_secs: entry.joined_at_secs,
524 connected,
525 });
526 }
527 operators.sort_by(|a, b| a.sid.as_str().cmp(b.sid.as_str()));
528 (StatusCode::OK, Json(OperatorsListResp { operators })).into_response()
529}
530
531pub async fn operators_delete_by_role(
558 State(state): State<AppState>,
559 Path(role): Path<String>,
560 axum::extract::Query(query): axum::extract::Query<OperatorsDeleteByRoleQuery>,
561) -> Response {
562 let sid = {
563 let map = state.roles_to_sid.lock().await;
564 match map.get(role.as_str()) {
565 Some(sid) => sid.clone(),
566 None => {
567 return (
568 StatusCode::NOT_FOUND,
569 Json(json!({"error": "no session holds this role", "role": role})),
570 )
571 .into_response();
572 }
573 }
574 };
575 let entry = {
576 let map = state.operator_sessions.lock().await;
577 map.get(&sid).cloned()
578 };
579 let entry = match entry {
580 Some(e) => e,
581 None => {
582 let mut map = state.roles_to_sid.lock().await;
588 if map.get(role.as_str()) == Some(&sid) {
589 map.remove(role.as_str());
590 }
591 return (
592 StatusCode::NOT_FOUND,
593 Json(json!({
594 "error": "torn role mapping cleared; role now open",
595 "role": role,
596 })),
597 )
598 .into_response();
599 }
600 };
601 if !query.force {
602 match active_runs_for_sid(&state, &sid).await {
603 Ok(active_runs) if !active_runs.is_empty() => {
604 return (
605 StatusCode::CONFLICT,
606 Json(json!({
607 "error": "session is driving in-flight runs; \
608 tearing it down would fail their parked spawns",
609 "role": role,
610 "sid": sid,
611 "active_runs": active_runs,
612 "hint": "wait for the runs to finish, or repeat with ?force=true \
613 to tear the session down anyway",
614 })),
615 )
616 .into_response();
617 }
618 Ok(_) => {}
619 Err(error) => {
620 tracing::warn!(%role, %sid, %error, "operators_delete_by_role: run occupancy check failed");
625 return (
626 StatusCode::CONFLICT,
627 Json(json!({
628 "error": format!(
629 "cannot verify whether this session is driving in-flight runs: {error}"
630 ),
631 "role": role,
632 "sid": sid,
633 "hint": "retry, or repeat with ?force=true to tear the session down \
634 without the check",
635 })),
636 )
637 .into_response();
638 }
639 }
640 }
641 teardown_operator_session(&state, &sid, &entry).await;
642 StatusCode::NO_CONTENT.into_response()
643}
644
645#[derive(Debug, Deserialize, Default)]
647pub struct OperatorsDeleteByRoleQuery {
648 #[serde(default)]
652 pub force: bool,
653}
654
655async fn active_runs_for_sid(state: &AppState, sid: &SessionId) -> Result<Vec<String>, String> {
659 let mut running = state
660 .run_store
661 .list_running()
662 .await
663 .map_err(|e| e.to_string())?;
664 running.sort_by_key(|record| record.created_at);
665 Ok(running
666 .into_iter()
667 .filter(|record| record.operator_sid.as_deref() == Some(sid.as_str()))
668 .map(|record| record.id.to_string())
669 .collect())
670}
671
672#[derive(Debug, Serialize)]
676pub struct OperatorsInfoResp {
677 pub sid: SessionId,
679 pub roles: Vec<String>,
681 #[serde(skip_serializing_if = "Option::is_none")]
683 pub capability_manifest: Option<AgentProviderManifest>,
684 pub connected: bool,
686}
687
688pub async fn operators_info(
692 State(state): State<AppState>,
693 Path(sid): Path<String>,
694 headers: HeaderMap,
695) -> Response {
696 let bearer = match extract_bearer_token_required(&headers) {
697 Ok(t) => t,
698 Err(resp) => return *resp,
699 };
700 let Ok(sid) = SessionId::parse(sid) else {
701 return (StatusCode::NOT_FOUND, "unknown sid").into_response();
702 };
703
704 let entry = {
705 let map = state.operator_sessions.lock().await;
706 map.get(&sid).cloned()
707 };
708 let entry = match entry {
709 Some(e) => e,
710 None => return (StatusCode::NOT_FOUND, "unknown sid").into_response(),
711 };
712 if !mlua_swarm::types::ct_eq(entry.token.as_bytes(), bearer.as_bytes()) {
713 return (StatusCode::UNAUTHORIZED, "token mismatch").into_response();
714 }
715
716 let session = entry.ws_session.lock().await.clone();
717 let connected = match session {
718 Some(session) => session.is_connected().await,
719 None => false,
720 };
721 (
722 StatusCode::OK,
723 Json(OperatorsInfoResp {
724 sid: entry.sid.clone(),
725 roles: entry.roles.clone(),
726 capability_manifest: entry.capability_manifest.clone(),
727 connected,
728 }),
729 )
730 .into_response()
731}
732
733#[cfg(test)]
734mod tests {
735 use super::*;
736 use axum::http::HeaderValue;
737
738 fn headers_with_bearer(token: &str) -> HeaderMap {
739 let mut h = HeaderMap::new();
740 h.insert(
741 axum::http::header::AUTHORIZATION,
742 HeaderValue::from_str(&format!("Bearer {token}")).unwrap(),
743 );
744 h
745 }
746
747 #[test]
748 fn extract_bearer_token_required_accepts_valid() {
749 let h = headers_with_bearer("abc123");
750 assert_eq!(extract_bearer_token_required(&h).unwrap(), "abc123");
751 }
752
753 #[test]
754 fn extract_bearer_token_required_rejects_missing_header() {
755 let h = HeaderMap::new();
756 assert!(extract_bearer_token_required(&h).is_err());
757 }
758
759 #[test]
760 fn extract_bearer_token_required_rejects_empty_token() {
761 let h = headers_with_bearer("");
762 assert!(extract_bearer_token_required(&h).is_err());
763 }
764
765 #[test]
766 fn extract_bearer_token_required_rejects_wrong_scheme() {
767 let mut h = HeaderMap::new();
768 h.insert(
769 axum::http::header::AUTHORIZATION,
770 HeaderValue::from_static("Basic dXNlcjpwYXNz"),
771 );
772 assert!(extract_bearer_token_required(&h).is_err());
773 }
774
775 #[test]
776 fn operators_create_request_accepts_capability_manifest() {
777 let req: OperatorsCreateReq = serde_json::from_value(serde_json::json!({
778 "roles": ["main-ai"],
779 "capability_manifest": {
780 "provider_id": "main-ai-self-report",
781 "capabilities": [{
782 "launch_variant": "mse-coder",
783 "resolved_model": "claude-sonnet-4",
784 "effective_tools": ["Read", "Edit"]
785 }]
786 }
787 }))
788 .unwrap();
789 assert_eq!(req.roles, ["main-ai"]);
790 assert_eq!(
791 req.capability_manifest.unwrap().provider_id,
792 "main-ai-self-report"
793 );
794 }
795
796 #[test]
797 fn operators_create_request_keeps_manifest_optional_on_wire() {
798 let req: OperatorsCreateReq =
799 serde_json::from_value(serde_json::json!({ "roles": [] })).unwrap();
800 assert!(req.capability_manifest.is_none());
801 }
802
803 mod by_role_in_flight {
806 use super::*;
807 use mlua_swarm::core::config::EngineCfg;
808 use mlua_swarm::core::engine::Engine;
809 use mlua_swarm::store::output::InMemoryOutputStore;
810 use mlua_swarm::store::run::{InMemoryRunStore, RunRecord, RunStatus};
811 use mlua_swarm::store::task::InMemoryTaskStore;
812 use mlua_swarm::RunId;
813 use mlua_swarm::TaskId;
814 use std::collections::HashMap;
815
816 fn test_state() -> AppState {
817 let engine =
818 Engine::new_with_layers(EngineCfg::default(), crate::default_layer_registry());
819 let compiler = mlua_swarm::Compiler::new(crate::default_registry());
820 let launch = Arc::new(mlua_swarm::TaskLaunchService::new(engine.clone(), compiler));
821 AppState {
822 engine,
823 sessions: Arc::new(Mutex::new(crate::SessionStore::default())),
824 task_app: Arc::new(mlua_swarm::TaskApplication::new_inline_only(launch)),
825 ws_operator_factory: None,
826 data_store: Arc::new(InMemoryOutputStore::new()),
827 operator_sessions: Arc::new(Mutex::new(HashMap::new())),
828 roles_to_sid: Arc::new(Mutex::new(HashMap::new())),
829 task_store: Arc::new(InMemoryTaskStore::new()),
830 run_store: Arc::new(InMemoryRunStore::new()),
831 replay_store: Arc::new(mlua_swarm::store::replay::InMemoryReplayStore::new()),
832 run_trace_store: Arc::new(mlua_swarm::store::trace::InMemoryRunTraceStore::new()),
833 base_url: None,
834 sync_timeout_secs: 300,
835 }
836 }
837
838 async fn seed_session(state: &AppState, role: &str) -> SessionId {
841 let sid = SessionId::new();
842 let entry = Arc::new(OperatorSessionEntry {
843 sid: sid.clone(),
844 token: "token".to_string(),
845 roles: vec![role.to_string()],
846 capability_manifest: None,
847 joined_at_secs: 0,
848 ws_session: Mutex::new(None),
849 });
850 state
851 .operator_sessions
852 .lock()
853 .await
854 .insert(sid.clone(), entry);
855 state
856 .roles_to_sid
857 .lock()
858 .await
859 .insert(role.to_string(), sid.clone());
860 sid
861 }
862
863 async fn seed_run(state: &AppState, sid: Option<&SessionId>, status: RunStatus) -> RunId {
864 let run_id = RunId::new();
865 state
866 .run_store
867 .create(RunRecord {
868 id: run_id.clone(),
869 task_id: TaskId::new(),
870 status,
871 step_entries: Vec::new(),
872 degradations: Vec::new(),
873 operator_sid: sid.map(|s| s.to_string()),
874 result_ref: None,
875 input_json: None,
876 created_at: 0,
877 updated_at: 0,
878 })
879 .await
880 .expect("seed run");
881 run_id
882 }
883
884 async fn body_json(response: Response) -> serde_json::Value {
885 let bytes = axum::body::to_bytes(response.into_body(), usize::MAX)
886 .await
887 .expect("read body");
888 serde_json::from_slice(&bytes).expect("json body")
889 }
890
891 async fn session_is_live(state: &AppState, sid: &SessionId) -> bool {
892 state.operator_sessions.lock().await.contains_key(sid)
893 }
894
895 #[tokio::test]
898 async fn idle_holder_is_still_torn_down() {
899 let state = test_state();
900 let sid = seed_session(&state, "main-ai").await;
901 let other = SessionId::new();
904 seed_run(&state, Some(&other), RunStatus::Running).await;
905 seed_run(&state, Some(&sid), RunStatus::Done).await;
907
908 let response = operators_delete_by_role(
909 State(state.clone()),
910 Path("main-ai".to_string()),
911 axum::extract::Query(OperatorsDeleteByRoleQuery::default()),
912 )
913 .await;
914 assert_eq!(response.status(), StatusCode::NO_CONTENT);
915 assert!(!session_is_live(&state, &sid).await);
916 }
917
918 #[tokio::test]
921 async fn holder_driving_a_running_run_is_refused_with_its_run_ids() {
922 let state = test_state();
923 let sid = seed_session(&state, "main-ai").await;
924 let run_id = seed_run(&state, Some(&sid), RunStatus::Running).await;
925
926 let response = operators_delete_by_role(
927 State(state.clone()),
928 Path("main-ai".to_string()),
929 axum::extract::Query(OperatorsDeleteByRoleQuery::default()),
930 )
931 .await;
932 assert_eq!(response.status(), StatusCode::CONFLICT);
933 let body = body_json(response).await;
934 assert_eq!(
935 body["active_runs"],
936 serde_json::json!([run_id.to_string()]),
937 "the 409 must name the in-flight runs: {body}"
938 );
939 assert!(
940 session_is_live(&state, &sid).await,
941 "a refused teardown must leave the session (and its parked spawns) alone"
942 );
943 assert_eq!(
945 state.roles_to_sid.lock().await.get("main-ai"),
946 Some(&sid),
947 "a refused teardown must not release the role"
948 );
949 }
950
951 #[tokio::test]
954 async fn force_tears_down_despite_in_flight_runs() {
955 let state = test_state();
956 let sid = seed_session(&state, "main-ai").await;
957 seed_run(&state, Some(&sid), RunStatus::Running).await;
958
959 let response = operators_delete_by_role(
960 State(state.clone()),
961 Path("main-ai".to_string()),
962 axum::extract::Query(OperatorsDeleteByRoleQuery { force: true }),
963 )
964 .await;
965 assert_eq!(response.status(), StatusCode::NO_CONTENT);
966 assert!(!session_is_live(&state, &sid).await);
967 }
968
969 #[tokio::test]
972 async fn unknown_role_still_404s() {
973 let state = test_state();
974 let response = operators_delete_by_role(
975 State(state),
976 Path("nobody-holds-this".to_string()),
977 axum::extract::Query(OperatorsDeleteByRoleQuery::default()),
978 )
979 .await;
980 assert_eq!(response.status(), StatusCode::NOT_FOUND);
981 }
982 }
983}