use std::time::Duration;
use ironflow_store::entities::Assignee;
use serde::{Deserialize, Serialize};
use super::{ApprovalConfig, Approvers, EscalationPolicy};
pub const HUMAN_INPUT_SCHEMA_KEY: &str = "schema";
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HumanInputConfig {
#[serde(flatten)]
gate: ApprovalConfig,
}
impl HumanInputConfig {
pub fn new(message: &str) -> Self {
Self {
gate: ApprovalConfig::new(message),
}
}
pub fn with_deadline(self, deadline: Duration) -> Self {
Self {
gate: self.gate.with_deadline(deadline),
}
}
pub fn with_deadline_secs(self, secs: u64) -> Self {
Self {
gate: self.gate.with_deadline_secs(secs),
}
}
pub fn on_timeout(self, policy: EscalationPolicy) -> Self {
assert!(
!allows_auto_approve(&policy),
"a human input step cannot be auto-approved on timeout"
);
Self {
gate: self.gate.on_timeout(policy),
}
}
pub fn assigned_to(self, assignee: Assignee) -> Self {
Self {
gate: self.gate.assigned_to(assignee),
}
}
pub fn requiring(self, approvers: Approvers) -> Self {
Self {
gate: self.gate.requiring(approvers),
}
}
pub fn message(&self) -> &str {
self.gate.message()
}
pub fn deadline(&self) -> Option<Duration> {
self.gate.deadline()
}
pub fn deadline_secs(&self) -> Option<u64> {
self.gate.deadline_secs()
}
pub fn on_timeout_policy(&self) -> Option<&EscalationPolicy> {
self.gate.on_timeout_policy()
}
pub fn assignee(&self) -> Option<&Assignee> {
self.gate.assignee()
}
pub fn approvers(&self) -> Option<&Approvers> {
self.gate.approvers()
}
pub fn effective_deadline_secs(&self) -> Option<u64> {
self.gate.effective_deadline_secs()
}
pub fn effective_policy(&self) -> EscalationPolicy {
self.gate.effective_policy()
}
}
fn allows_auto_approve(policy: &EscalationPolicy) -> bool {
match policy {
EscalationPolicy::AutoApprove => true,
EscalationPolicy::Chain(policies) => policies.iter().any(allows_auto_approve),
_ => false,
}
}
#[cfg(test)]
mod tests {
use serde_json::{from_str, from_value, json, to_string, to_value};
use super::*;
use crate::config::NotificationTarget;
fn webhook() -> EscalationPolicy {
EscalationPolicy::Notify(vec![NotificationTarget::Webhook {
url: "https://example.com/sla".to_string(),
}])
}
#[test]
fn new_sets_message() {
let config = HumanInputConfig::new("Answer?");
assert_eq!(config.message(), "Answer?");
assert!(config.deadline_secs().is_none());
assert!(config.on_timeout_policy().is_none());
assert!(config.assignee().is_none());
assert!(config.approvers().is_none());
}
#[test]
fn builder_values_are_stored() {
let config = HumanInputConfig::new("Answer?")
.with_deadline(Duration::from_secs(120))
.on_timeout(EscalationPolicy::AutoReject)
.assigned_to(Assignee::group("product"))
.requiring(Approvers::at_least(2).from_groups(["product"]));
assert_eq!(config.deadline(), Some(Duration::from_secs(120)));
assert_eq!(config.effective_deadline_secs(), Some(120));
assert_eq!(
config.on_timeout_policy(),
Some(&EscalationPolicy::AutoReject)
);
assert_eq!(config.assignee(), Some(&Assignee::group("product")));
assert_eq!(config.approvers().map(Approvers::required), Some(2));
}
#[test]
fn with_deadline_secs_is_stored() {
let config = HumanInputConfig::new("Answer?").with_deadline_secs(30);
assert_eq!(config.deadline_secs(), Some(30));
}
#[test]
#[should_panic(expected = "approval deadline must be greater than zero")]
fn with_deadline_secs_rejects_zero() {
let _ = HumanInputConfig::new("Answer?").with_deadline_secs(0);
}
#[test]
fn on_timeout_accepts_policies_without_auto_approve() {
let reject = HumanInputConfig::new("Answer?").on_timeout(EscalationPolicy::AutoReject);
assert_eq!(reject.effective_policy(), EscalationPolicy::AutoReject);
let notify = HumanInputConfig::new("Answer?").on_timeout(webhook());
assert_eq!(notify.effective_policy(), webhook());
let chain = EscalationPolicy::Chain(vec![webhook(), EscalationPolicy::AutoReject]);
let chained = HumanInputConfig::new("Answer?").on_timeout(chain.clone());
assert_eq!(chained.effective_policy(), chain);
}
#[test]
#[should_panic(expected = "a human input step cannot be auto-approved on timeout")]
fn on_timeout_rejects_auto_approve() {
let _ = HumanInputConfig::new("Answer?").on_timeout(EscalationPolicy::AutoApprove);
}
#[test]
#[should_panic(expected = "a human input step cannot be auto-approved on timeout")]
fn on_timeout_rejects_a_chain_with_auto_approve() {
let _ = HumanInputConfig::new("Answer?").on_timeout(EscalationPolicy::Chain(vec![
webhook(),
EscalationPolicy::AutoApprove,
]));
}
#[test]
fn serde_roundtrip() {
let config = HumanInputConfig::new("Answer?")
.with_deadline_secs(600)
.assigned_to(Assignee::user("alice"))
.requiring(Approvers::any().from_groups(["product"]));
let json = to_string(&config).expect("serialize");
let back: HumanInputConfig = from_str(&json).expect("deserialize");
assert_eq!(back.message(), config.message());
assert_eq!(back.deadline_secs(), config.deadline_secs());
assert_eq!(back.assignee(), config.assignee());
assert_eq!(back.approvers(), config.approvers());
assert_eq!(to_string(&back).expect("serialize"), json);
}
#[test]
fn serialized_config_reads_as_an_approval_config() {
let config = HumanInputConfig::new("Answer?").with_deadline_secs(600);
let mut value = to_value(&config).expect("serialize");
value.as_object_mut().expect("object").insert(
HUMAN_INPUT_SCHEMA_KEY.to_string(),
json!({"type": "object"}),
);
let approval: ApprovalConfig = from_value(value).expect("deserialize");
assert_eq!(approval.message(), "Answer?");
assert_eq!(approval.deadline_secs(), Some(600));
}
}