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