1use axum::Json;
6use axum::extract::{Path, State};
7use axum::response::IntoResponse;
8use chrono::Utc;
9use ironflow_auth::extractor::Authenticated;
10use ironflow_engine::config::HUMAN_INPUT_SCHEMA_KEY;
11use ironflow_engine::engine::ExecutionMode;
12use ironflow_store::entities::ApiKeyScope;
13use ironflow_store::models::{
14 Run, RunStatus, Step, StepApproval, StepKind, StepStatus, StepUpdate,
15};
16use jsonschema::validator_for;
17use serde::{Deserialize, Serialize};
18use serde_json::Value;
19use tokio::spawn;
20use tracing::error;
21use uuid::Uuid;
22
23use crate::authorization::AuthorizeExt;
24use crate::entities::RunResponse;
25use crate::error::ApiError;
26use crate::response::ok;
27use crate::routes::approve_run::{authorize_gate, awaits_gate, resume_paused_run_later};
28use crate::state::AppState;
29
30#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
32#[derive(Debug, Default, Deserialize, Serialize)]
33pub struct RejectHumanInputRequest {
34 #[serde(default)]
36 pub reason: Option<String>,
37}
38
39#[cfg_attr(
55 feature = "openapi",
56 utoipa::path(
57 post,
58 path = "/api/v1/runs/{id}/steps/{step_id}/input",
59 tags = ["runs"],
60 params(
61 ("id" = Uuid, Path, description = "Run ID"),
62 ("step_id" = Uuid, Path, description = "Step ID")
63 ),
64 request_body(content = Value, content_type = "application/json", description = "Answer matching the JSON schema stored on the step"),
65 responses(
66 (status = 200, description = "Answer recorded, the run resumes", body = RunResponse),
67 (status = 400, description = "Step is not a human input awaiting input"),
68 (status = 401, description = "Unauthorized"),
69 (status = 403, description = "Forbidden"),
70 (status = 404, description = "Run or step not found"),
71 (status = 409, description = "Input already answered or rejected"),
72 (status = 422, description = "Answer does not match the schema")
73 ),
74 security(("Bearer" = []))
75 )
76)]
77pub async fn submit_human_input(
78 auth: Authenticated,
79 State(state): State<AppState>,
80 Path((id, step_id)): Path<(Uuid, Uuid)>,
81 Json(answer): Json<Value>,
82) -> Result<impl IntoResponse, ApiError> {
83 auth.require(ApiKeyScope::RunsManage, false)?;
84 let (run, step) = open_input_step(&state, id, step_id).await?;
85 let actor = authorize_gate(&auth, &state, &run, &step).await?;
86 validate_answer(step.input.as_ref(), &answer)?;
87
88 let now = Utc::now();
92 state
93 .store
94 .record_step_approval(
95 step.id,
96 StepApproval {
97 user_id: auth.user_id,
98 approved_by: actor,
99 at: now,
100 },
101 )
102 .await?;
103 state
104 .store
105 .update_step(
106 step.id,
107 StepUpdate {
108 status: Some(StepStatus::Completed),
109 output: Some(answer),
110 completed_at: Some(now),
111 clear_approval_deadline: true,
112 ..StepUpdate::default()
113 },
114 )
115 .await?;
116 release_run(&state, &run).await?;
117
118 Ok(ok(RunResponse::from(state.get_run_or_404(id).await?)))
119}
120
121#[cfg_attr(
135 feature = "openapi",
136 utoipa::path(
137 post,
138 path = "/api/v1/runs/{id}/steps/{step_id}/reject",
139 tags = ["runs"],
140 params(
141 ("id" = Uuid, Path, description = "Run ID"),
142 ("step_id" = Uuid, Path, description = "Step ID")
143 ),
144 request_body(content = RejectHumanInputRequest, description = "Optional rejection reason"),
145 responses(
146 (status = 200, description = "Input rejected, the run resumes", body = RunResponse),
147 (status = 400, description = "Step is not a human input awaiting input"),
148 (status = 401, description = "Unauthorized"),
149 (status = 403, description = "Forbidden"),
150 (status = 404, description = "Run or step not found"),
151 (status = 409, description = "Input already answered or rejected")
152 ),
153 security(("Bearer" = []))
154 )
155)]
156pub async fn reject_human_input(
157 auth: Authenticated,
158 State(state): State<AppState>,
159 Path((id, step_id)): Path<(Uuid, Uuid)>,
160 body: Option<Json<RejectHumanInputRequest>>,
161) -> Result<impl IntoResponse, ApiError> {
162 auth.require(ApiKeyScope::RunsManage, false)?;
163 let (run, step) = open_input_step(&state, id, step_id).await?;
164 let actor = authorize_gate(&auth, &state, &run, &step).await?;
165
166 let reason = body
167 .and_then(|Json(b)| b.reason)
168 .filter(|r| !r.trim().is_empty())
169 .unwrap_or_else(|| format!("input rejected by {actor}"));
170
171 state
172 .store
173 .update_step(
174 step.id,
175 StepUpdate {
176 status: Some(StepStatus::Rejected),
177 error: Some(reason),
178 completed_at: Some(Utc::now()),
179 clear_approval_deadline: true,
180 ..StepUpdate::default()
181 },
182 )
183 .await?;
184 release_run(&state, &run).await?;
185
186 Ok(ok(RunResponse::from(state.get_run_or_404(id).await?)))
187}
188
189async fn open_input_step(
194 state: &AppState,
195 run_id: Uuid,
196 step_id: Uuid,
197) -> Result<(Run, Step), ApiError> {
198 let run = state.get_run_or_404(run_id).await?;
199 let step = state
200 .store
201 .get_step(step_id)
202 .await?
203 .filter(|s| s.run_id == run_id)
204 .ok_or(ApiError::StepNotFound(step_id))?;
205
206 if step.kind != StepKind::HumanInput {
207 return Err(ApiError::BadRequest(
208 "step does not wait for input".to_string(),
209 ));
210 }
211
212 match step.status.state {
213 StepStatus::Completed => {
214 return Err(ApiError::Conflict("input already provided".to_string()));
215 }
216 StepStatus::Rejected => {
217 return Err(ApiError::Conflict("input already rejected".to_string()));
218 }
219 _ => {}
220 }
221
222 if step.status.state != StepStatus::AwaitingApproval || !awaits_gate(&run) {
223 return Err(ApiError::BadRequest(
224 "step is not awaiting input".to_string(),
225 ));
226 }
227
228 Ok((run, step))
229}
230
231fn validate_answer(input: Option<&Value>, answer: &Value) -> Result<(), ApiError> {
233 let schema = input
234 .and_then(|i| i.get(HUMAN_INPUT_SCHEMA_KEY))
235 .ok_or_else(|| ApiError::Internal("input step has no stored schema".to_string()))?;
236 let validator = validator_for(schema)
237 .map_err(|e| ApiError::Internal(format!("input step has an invalid schema: {e}")))?;
238
239 let errors: Vec<String> = validator
240 .iter_errors(answer)
241 .map(|e| e.to_string())
242 .collect();
243 if errors.is_empty() {
244 Ok(())
245 } else {
246 Err(ApiError::InvalidInput(errors))
247 }
248}
249
250async fn release_run(state: &AppState, run: &Run) -> Result<(), ApiError> {
255 if run.status.state == RunStatus::Paused {
256 return resume_paused_run_later(state, run.id).await;
257 }
258 state
259 .store
260 .update_run_status(run.id, resume_status(state))
261 .await?;
262 resume_in_background(state, run.id);
263 Ok(())
264}
265
266fn resume_status(state: &AppState) -> RunStatus {
269 match state.engine.execution_mode() {
270 ExecutionMode::Local => RunStatus::Running,
271 ExecutionMode::Workers => RunStatus::Pending,
272 }
273}
274
275fn resume_in_background(state: &AppState, id: Uuid) {
279 if !matches!(state.engine.execution_mode(), ExecutionMode::Local) {
280 return;
281 }
282 let engine = state.engine.clone();
283 spawn(async move {
284 if let Err(err) = engine.resume_run(id).await {
285 error!(run_id = %id, error = %err, "failed to resume run after human input");
286 }
287 });
288}
289
290#[cfg(test)]
291mod tests {
292 use std::collections::HashMap;
293 use std::sync::Arc;
294
295 use axum::Router;
296 use axum::body::Body;
297 use axum::http::{Request, StatusCode};
298 use axum::routing::post;
299 use http_body_util::BodyExt;
300 use ironflow_auth::jwt::{AccessToken, JwtConfig};
301 use ironflow_auth::password;
302 use ironflow_core::providers::claude::ClaudeCodeProvider;
303 use ironflow_engine::engine::Engine;
304 use ironflow_engine::handler::input_schema_for;
305 use ironflow_engine::notify::Event;
306 use ironflow_store::memory::InMemoryStore;
307 use ironflow_store::models::{
308 Assignee, NewRun, NewStep, NewUser, TriggerKind, User, step_trace_id,
309 };
310 use ironflow_store::store::RunStore;
311 use ironflow_store::user_store::UserStore;
312 use schemars::JsonSchema;
313 use serde_json::{from_slice, json};
314 use tokio::sync::broadcast;
315 use tower::ServiceExt;
316
317 use super::*;
318 use crate::routes::test_helpers::create_user_auth_header;
319
320 #[allow(dead_code)]
322 #[derive(Deserialize, JsonSchema)]
323 struct Answers {
324 answers: Vec<String>,
325 }
326
327 #[allow(dead_code)]
329 #[derive(Deserialize, JsonSchema)]
330 struct Nested {
331 target: Target,
332 }
333
334 #[allow(dead_code)]
335 #[derive(Deserialize, JsonSchema)]
336 struct Target {
337 env: String,
338 }
339
340 fn test_state(store: Arc<InMemoryStore>) -> AppState {
341 let provider = Arc::new(ClaudeCodeProvider::new());
342 let engine = Arc::new(Engine::new(store.clone(), provider));
343 let jwt_config = Arc::new(JwtConfig {
344 secret: "test-secret".to_string(),
345 access_token_ttl_secs: 900,
346 refresh_token_ttl_secs: 604800,
347 cookie_domain: None,
348 cookie_secure: false,
349 });
350 let (event_sender, _) = broadcast::channel::<Event>(1);
351 AppState::new(
352 store,
353 engine,
354 jwt_config,
355 "test-worker-token".to_string(),
356 event_sender,
357 )
358 }
359
360 async fn member(store: &Arc<InMemoryStore>, username: &str) -> User {
361 let password_hash = password::hash("password123").expect("hash");
362 store
363 .create_user(NewUser {
364 email: format!("{username}@example.com"),
365 username: username.to_string(),
366 password_hash,
367 is_admin: Some(false),
369 })
370 .await
371 .expect("create user")
372 }
373
374 fn member_header(user: &User, state: &AppState) -> String {
375 let token = AccessToken::for_user(user.id, &user.username, false, &state.jwt_config)
376 .expect("token");
377 format!("Bearer {}", token.0)
378 }
379
380 async fn awaiting_run(store: &Arc<InMemoryStore>) -> Uuid {
381 let run = store
382 .create_run(NewRun {
383 created_by: None,
384 workflow_name: "test".to_string(),
385 trigger: TriggerKind::Manual,
386 payload: json!({}),
387 max_retries: 0,
388 handler_version: None,
389 labels: HashMap::new(),
390 scheduled_at: None,
391 idempotency_key: None,
392 concurrency_key: None,
393 priority: 0,
394 concurrency_limits: Vec::new(),
395 max_cost_usd: None,
396 worker_tags: Vec::new(),
397 })
398 .await
399 .unwrap()
400 .into_run();
401 store
402 .update_run_status(run.id, RunStatus::Running)
403 .await
404 .unwrap();
405 store
406 .update_run_status(run.id, RunStatus::AwaitingApproval)
407 .await
408 .unwrap();
409 run.id
410 }
411
412 async fn awaiting_step(
414 store: &Arc<InMemoryStore>,
415 run_id: Uuid,
416 kind: StepKind,
417 assignee: Option<Assignee>,
418 ) -> Uuid {
419 let step = store
420 .create_step(NewStep {
421 run_id,
422 trace_id: step_trace_id(run_id, "clarify", 0),
423 name: "clarify".to_string(),
424 kind,
425 position: 0,
426 input: Some(json!({
427 "message": "Answer the questions",
428 HUMAN_INPUT_SCHEMA_KEY: input_schema_for::<Answers>(),
429 })),
430 is_error_handler: false,
431 })
432 .await
433 .unwrap();
434 store
435 .update_step(
436 step.id,
437 StepUpdate {
438 status: Some(StepStatus::Running),
439 ..StepUpdate::default()
440 },
441 )
442 .await
443 .unwrap();
444 store
445 .update_step(
446 step.id,
447 StepUpdate {
448 status: Some(StepStatus::AwaitingApproval),
449 approval_assignee: assignee,
450 ..StepUpdate::default()
451 },
452 )
453 .await
454 .unwrap();
455 step.id
456 }
457
458 async fn setup() -> (Arc<InMemoryStore>, AppState, Uuid, Uuid) {
460 let store = Arc::new(InMemoryStore::new());
461 let run_id = awaiting_run(&store).await;
462 let step_id = awaiting_step(&store, run_id, StepKind::HumanInput, None).await;
463 let state = test_state(store.clone());
464 (store, state, run_id, step_id)
465 }
466
467 fn app(state: AppState) -> Router {
468 Router::new()
469 .route("/{id}/steps/{step_id}/input", post(submit_human_input))
470 .route("/{id}/steps/{step_id}/reject", post(reject_human_input))
471 .with_state(state)
472 }
473
474 async fn call(
476 state: &AppState,
477 auth: &str,
478 run_id: Uuid,
479 step_id: Uuid,
480 verb: &str,
481 body: Option<Value>,
482 ) -> (StatusCode, Value) {
483 let builder = Request::builder()
484 .method("POST")
485 .uri(format!("/{run_id}/steps/{step_id}/{verb}"))
486 .header("authorization", auth);
487 let req = match body {
488 Some(body) => builder
489 .header("content-type", "application/json")
490 .body(Body::from(body.to_string()))
491 .unwrap(),
492 None => builder.body(Body::empty()).unwrap(),
493 };
494
495 let resp = app(state.clone()).oneshot(req).await.unwrap();
496 let status = resp.status();
497 let bytes = resp.into_body().collect().await.unwrap().to_bytes();
498 let json_val = if bytes.is_empty() {
499 Value::Null
500 } else {
501 from_slice(&bytes).unwrap()
502 };
503 (status, json_val)
504 }
505
506 #[tokio::test]
507 async fn human_input_submit_valid_answer_resumes_the_run() {
508 let (store, state, run_id, step_id) = setup().await;
509 let auth = create_user_auth_header(&state, "admin", true).await;
510 let answer = json!({"answers": ["staging"]});
511
512 let (status, body) = call(
513 &state,
514 &auth,
515 run_id,
516 step_id,
517 "input",
518 Some(answer.clone()),
519 )
520 .await;
521
522 assert_eq!(status, StatusCode::OK);
523 assert_eq!(body["data"]["status"], "running");
524 let step = store.get_step(step_id).await.unwrap().unwrap();
525 assert_eq!(step.status.state, StepStatus::Completed);
526 assert_eq!(step.output, Some(answer));
527 assert_eq!(step.approvals.len(), 1);
528 assert_eq!(step.approvals[0].approved_by, "admin");
529 }
530
531 #[tokio::test]
532 async fn human_input_submit_invalid_answer_returns_422() {
533 let (store, state, run_id, step_id) = setup().await;
534 let auth = create_user_auth_header(&state, "admin", true).await;
535
536 for answer in [json!({"answers": 3}), json!({})] {
537 let (status, body) = call(&state, &auth, run_id, step_id, "input", Some(answer)).await;
538
539 assert_eq!(status, StatusCode::UNPROCESSABLE_ENTITY);
540 assert_eq!(body["error"]["code"], "INVALID_INPUT");
541 let errors = body["error"]["details"]["errors"]
542 .as_array()
543 .expect("errors array");
544 assert!(!errors.is_empty());
545 }
546
547 let step = store.get_step(step_id).await.unwrap().unwrap();
548 assert_eq!(step.status.state, StepStatus::AwaitingApproval);
549 let run = store.get_run(run_id).await.unwrap().unwrap();
550 assert_eq!(run.status.state, RunStatus::AwaitingApproval);
551 }
552
553 #[tokio::test]
554 async fn human_input_submit_twice_returns_409() {
555 let (_store, state, run_id, step_id) = setup().await;
556 let auth = create_user_auth_header(&state, "admin", true).await;
557 let answer = json!({"answers": ["yes"]});
558
559 let (first, _) = call(
560 &state,
561 &auth,
562 run_id,
563 step_id,
564 "input",
565 Some(answer.clone()),
566 )
567 .await;
568 assert_eq!(first, StatusCode::OK);
569
570 let (second, body) = call(&state, &auth, run_id, step_id, "input", Some(answer)).await;
571 assert_eq!(second, StatusCode::CONFLICT);
572 assert_eq!(body["error"]["message"], "input already provided");
573 }
574
575 #[tokio::test]
576 async fn human_input_submit_on_an_approval_step_returns_400() {
577 let store = Arc::new(InMemoryStore::new());
578 let run_id = awaiting_run(&store).await;
579 let step_id = awaiting_step(&store, run_id, StepKind::Approval, None).await;
580 let state = test_state(store);
581 let auth = create_user_auth_header(&state, "admin", true).await;
582
583 let answer = json!({"answers": []});
584 let (status, _) = call(&state, &auth, run_id, step_id, "input", Some(answer)).await;
585 assert_eq!(status, StatusCode::BAD_REQUEST);
586 }
587
588 #[tokio::test]
589 async fn human_input_submit_on_a_run_not_awaiting_returns_400() {
590 let (store, state, run_id, step_id) = setup().await;
591 store
592 .update_run_status(run_id, RunStatus::Running)
593 .await
594 .unwrap();
595 let auth = create_user_auth_header(&state, "admin", true).await;
596
597 let answer = json!({"answers": []});
598 let (status, _) = call(&state, &auth, run_id, step_id, "input", Some(answer)).await;
599 assert_eq!(status, StatusCode::BAD_REQUEST);
600 }
601
602 #[tokio::test]
603 async fn human_input_unknown_step_or_step_of_another_run_returns_404() {
604 let (store, state, run_id, step_id) = setup().await;
605 let auth = create_user_auth_header(&state, "admin", true).await;
606 let answer = json!({"answers": []});
607
608 let unknown = Uuid::now_v7();
609 let (status, body) = call(
610 &state,
611 &auth,
612 run_id,
613 unknown,
614 "input",
615 Some(answer.clone()),
616 )
617 .await;
618 assert_eq!(status, StatusCode::NOT_FOUND);
619 assert_eq!(body["error"]["code"], "STEP_NOT_FOUND");
620
621 let other_run = awaiting_run(&store).await;
622 let (status, _) = call(&state, &auth, other_run, step_id, "input", Some(answer)).await;
623 assert_eq!(status, StatusCode::NOT_FOUND);
624 }
625
626 #[tokio::test]
627 async fn human_input_unknown_run_returns_404() {
628 let (_store, state, _run_id, step_id) = setup().await;
629 let auth = create_user_auth_header(&state, "admin", true).await;
630
631 let answer = json!({"answers": []});
632 let (status, body) = call(
633 &state,
634 &auth,
635 Uuid::now_v7(),
636 step_id,
637 "input",
638 Some(answer),
639 )
640 .await;
641 assert_eq!(status, StatusCode::NOT_FOUND);
642 assert_eq!(body["error"]["code"], "RUN_NOT_FOUND");
643 }
644
645 #[tokio::test]
646 async fn human_input_non_admin_non_assignee_is_forbidden() {
647 let store = Arc::new(InMemoryStore::new());
648 let alice = member(&store, "alice").await;
649 let bob = member(&store, "bob").await;
650 let run_id = awaiting_run(&store).await;
651 let assignee = Some(Assignee::user(&alice.username));
652 let step_id = awaiting_step(&store, run_id, StepKind::HumanInput, assignee).await;
653 let state = test_state(store.clone());
654
655 let answer = json!({"answers": ["yes"]});
656 let bob_auth = member_header(&bob, &state);
657 let (status, _) = call(
658 &state,
659 &bob_auth,
660 run_id,
661 step_id,
662 "input",
663 Some(answer.clone()),
664 )
665 .await;
666 assert_eq!(status, StatusCode::FORBIDDEN);
667
668 let alice_auth = member_header(&alice, &state);
669 let (status, _) = call(&state, &alice_auth, run_id, step_id, "input", Some(answer)).await;
670 assert_eq!(status, StatusCode::OK);
671 let step = store.get_step(step_id).await.unwrap().unwrap();
672 assert_eq!(step.approvals[0].user_id, alice.id);
673 }
674
675 #[tokio::test]
676 async fn human_input_submit_on_a_paused_run_keeps_it_paused() {
677 let (store, state, run_id, step_id) = setup().await;
678 store
679 .update_run_status(run_id, RunStatus::Paused)
680 .await
681 .unwrap();
682 let auth = create_user_auth_header(&state, "admin", true).await;
683 let answer = json!({"answers": ["staging"]});
684
685 let (status, body) = call(&state, &auth, run_id, step_id, "input", Some(answer)).await;
686
687 assert_eq!(status, StatusCode::OK);
688 assert_eq!(body["data"]["status"], "paused");
689 assert_eq!(body["data"]["resume_status"], "pending");
690 let step = store.get_step(step_id).await.unwrap().unwrap();
691 assert_eq!(step.status.state, StepStatus::Completed);
692 let run = store.get_run(run_id).await.unwrap().unwrap();
693 assert_eq!(run.status.state, RunStatus::Paused);
694 assert_eq!(run.resume_status, Some(RunStatus::Pending));
695 }
696
697 #[tokio::test]
698 async fn human_input_reject_marks_the_step_and_resumes_the_run() {
699 let (store, state, run_id, step_id) = setup().await;
700 let auth = create_user_auth_header(&state, "admin", true).await;
701
702 let body = json!({"reason": "out of scope"});
703 let (status, resp) = call(&state, &auth, run_id, step_id, "reject", Some(body)).await;
704
705 assert_eq!(status, StatusCode::OK);
706 assert_eq!(resp["data"]["status"], "running");
707 let step = store.get_step(step_id).await.unwrap().unwrap();
708 assert_eq!(step.status.state, StepStatus::Rejected);
709 assert_eq!(step.error.as_deref(), Some("out of scope"));
710 assert!(step.approval_deadline_at.is_none());
711 }
712
713 async fn workers_setup() -> (Arc<InMemoryStore>, AppState, Uuid, Uuid, String) {
716 let store = Arc::new(InMemoryStore::new());
717 let alice = member(&store, "alice").await;
718 let run_id = awaiting_run(&store).await;
719 let assignee = Some(Assignee::user(&alice.username));
720 let step_id = awaiting_step(&store, run_id, StepKind::HumanInput, assignee).await;
721 let provider = Arc::new(ClaudeCodeProvider::new());
722 let engine =
723 Engine::new(store.clone(), provider).with_execution_mode(ExecutionMode::Workers);
724 let jwt_config = Arc::new(JwtConfig {
725 secret: "test-secret".to_string(),
726 access_token_ttl_secs: 900,
727 refresh_token_ttl_secs: 604800,
728 cookie_domain: None,
729 cookie_secure: false,
730 });
731 let (event_sender, _) = broadcast::channel::<Event>(1);
732 let state = AppState::new(
733 store.clone(),
734 Arc::new(engine),
735 jwt_config,
736 "test-worker-token".to_string(),
737 event_sender,
738 );
739 let auth = member_header(&alice, &state);
740 (store, state, run_id, step_id, auth)
741 }
742
743 #[tokio::test]
744 async fn human_input_submit_in_workers_mode_requeues_the_run() {
745 let (store, state, run_id, step_id, auth) = workers_setup().await;
746
747 let answer = json!({"answers": ["ok"]});
748 let (status, body) = call(&state, &auth, run_id, step_id, "input", Some(answer)).await;
749
750 assert_eq!(status, StatusCode::OK);
751 assert_eq!(body["data"]["status"], "pending");
752 let run = store.get_run(run_id).await.unwrap().unwrap();
753 assert_eq!(run.status.state, RunStatus::Pending);
754 let step = store.get_step(step_id).await.unwrap().unwrap();
755 assert_eq!(step.status.state, StepStatus::Completed);
756 }
757
758 #[tokio::test]
759 async fn human_input_reject_in_workers_mode_requeues_the_run() {
760 let (store, state, run_id, step_id, auth) = workers_setup().await;
761
762 let body = json!({"reason": "out of scope"});
763 let (status, resp) = call(&state, &auth, run_id, step_id, "reject", Some(body)).await;
764
765 assert_eq!(status, StatusCode::OK);
766 assert_eq!(resp["data"]["status"], "pending");
767 let run = store.get_run(run_id).await.unwrap().unwrap();
768 assert_eq!(run.status.state, RunStatus::Pending);
769 let step = store.get_step(step_id).await.unwrap().unwrap();
770 assert_eq!(step.status.state, StepStatus::Rejected);
771 }
772
773 #[tokio::test]
774 async fn human_input_reject_without_body_uses_the_default_reason() {
775 let (store, state, run_id, step_id) = setup().await;
776 let auth = create_user_auth_header(&state, "admin", true).await;
777
778 let (status, _) = call(&state, &auth, run_id, step_id, "reject", None).await;
779
780 assert_eq!(status, StatusCode::OK);
781 let step = store.get_step(step_id).await.unwrap().unwrap();
782 assert_eq!(step.error.as_deref(), Some("input rejected by admin"));
783 }
784
785 #[tokio::test]
786 async fn human_input_reject_with_a_blank_reason_uses_the_default_reason() {
787 let (store, state, run_id, step_id) = setup().await;
788 let auth = create_user_auth_header(&state, "admin", true).await;
789
790 let body = json!({"reason": " "});
791 let (status, _) = call(&state, &auth, run_id, step_id, "reject", Some(body)).await;
792
793 assert_eq!(status, StatusCode::OK);
794 let step = store.get_step(step_id).await.unwrap().unwrap();
795 assert_eq!(step.error.as_deref(), Some("input rejected by admin"));
796 }
797
798 #[tokio::test]
799 async fn human_input_reject_after_submit_returns_409() {
800 let (_store, state, run_id, step_id) = setup().await;
801 let auth = create_user_auth_header(&state, "admin", true).await;
802
803 let answer = json!({"answers": ["yes"]});
804 let (first, _) = call(&state, &auth, run_id, step_id, "input", Some(answer)).await;
805 assert_eq!(first, StatusCode::OK);
806
807 let (second, _) = call(&state, &auth, run_id, step_id, "reject", None).await;
808 assert_eq!(second, StatusCode::CONFLICT);
809 }
810
811 #[tokio::test]
812 async fn human_input_submit_after_reject_returns_409() {
813 let (_store, state, run_id, step_id) = setup().await;
814 let auth = create_user_auth_header(&state, "admin", true).await;
815
816 let (first, _) = call(&state, &auth, run_id, step_id, "reject", None).await;
817 assert_eq!(first, StatusCode::OK);
818
819 let answer = json!({"answers": ["yes"]});
820 let (second, body) = call(&state, &auth, run_id, step_id, "input", Some(answer)).await;
821 assert_eq!(second, StatusCode::CONFLICT);
822 assert_eq!(body["error"]["message"], "input already rejected");
823 }
824
825 #[test]
826 fn human_input_validate_answer_without_a_schema_is_internal() {
827 let err = validate_answer(Some(&json!({"message": "Answer?"})), &json!({}))
828 .expect_err("no schema");
829 assert!(matches!(err, ApiError::Internal(_)), "got {err:?}");
830
831 let err = validate_answer(None, &json!({})).expect_err("no input");
832 assert!(matches!(err, ApiError::Internal(_)), "got {err:?}");
833 }
834
835 #[test]
836 fn human_input_validate_answer_resolves_defs_references() {
837 let schema = input_schema_for::<Nested>();
838 assert!(schema.get("$defs").is_some(), "schema: {schema}");
839 let input = json!({ HUMAN_INPUT_SCHEMA_KEY: schema });
840
841 let valid = validate_answer(Some(&input), &json!({"target": {"env": "prod"}}));
842 assert!(valid.is_ok(), "got {valid:?}");
843
844 let err = validate_answer(Some(&input), &json!({"target": {"env": 1}}))
845 .expect_err("wrong nested type");
846 match err {
847 ApiError::InvalidInput(errors) => assert_eq!(errors.len(), 1, "got {errors:?}"),
848 other => panic!("expected InvalidInput, got {other:?}"),
849 }
850 }
851}