mockforge_intelligence/behavioral_economics/
actions.rs1use mockforge_foundation::Result;
7use serde::{Deserialize, Serialize};
8
9#[derive(Debug, Clone, Serialize, Deserialize)]
13#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
14#[serde(tag = "type", rename_all = "snake_case")]
15pub enum BehaviorAction {
16 NoOp,
18
19 ModifyConversionRate {
21 multiplier: f64,
23 },
24
25 DeclineTransaction {
27 reason: String,
29 },
30
31 IncreaseChurnProbability {
33 factor: f64,
35 },
36
37 ChangeResponseStatus {
39 status: u16,
41 },
42
43 ModifyLatency {
45 adjustment_ms: i64,
47 },
48
49 TriggerChaosRule {
51 rule_name: String,
53 },
54
55 ModifyResponseBody {
57 path: String,
59 value: String,
61 },
62}
63
64#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
66#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
67#[serde(tag = "type", rename_all = "snake_case")]
68pub enum ActionEffect {
69 None,
71 RateMultiplier {
73 target: String,
75 multiplier: f64,
77 },
78 Rejection {
80 reason: String,
82 },
83 StatusOverride {
85 status: u16,
87 },
88 LatencyAdjustment {
90 delta_ms: i64,
92 },
93 Chaostrigger {
95 rule_name: String,
97 },
98 BodyPatch {
100 path: String,
102 value: String,
104 },
105}
106
107#[derive(Debug, Clone)]
110pub struct ActionResult {
111 pub description: String,
113 pub effect: ActionEffect,
115}
116
117pub struct ActionExecutor;
122
123impl ActionExecutor {
124 pub fn new() -> Self {
126 Self
127 }
128
129 pub fn execute_action(&self, action: &BehaviorAction) -> Result<ActionResult> {
131 match action {
132 BehaviorAction::NoOp => Ok(ActionResult {
133 description: "No operation".to_string(),
134 effect: ActionEffect::None,
135 }),
136
137 BehaviorAction::ModifyConversionRate { multiplier } => Ok(ActionResult {
138 description: format!("Modified conversion rate by factor {}", multiplier),
139 effect: ActionEffect::RateMultiplier {
140 target: "conversion".to_string(),
141 multiplier: *multiplier,
142 },
143 }),
144
145 BehaviorAction::DeclineTransaction { reason } => Ok(ActionResult {
146 description: format!("Declined transaction: {}", reason),
147 effect: ActionEffect::Rejection {
148 reason: reason.clone(),
149 },
150 }),
151
152 BehaviorAction::IncreaseChurnProbability { factor } => Ok(ActionResult {
153 description: format!("Increased churn probability by factor {}", factor),
154 effect: ActionEffect::RateMultiplier {
155 target: "churn".to_string(),
156 multiplier: *factor,
157 },
158 }),
159
160 BehaviorAction::ChangeResponseStatus { status } => Ok(ActionResult {
161 description: format!("Changed response status to {}", status),
162 effect: ActionEffect::StatusOverride { status: *status },
163 }),
164
165 BehaviorAction::ModifyLatency { adjustment_ms } => Ok(ActionResult {
166 description: format!("Modified latency by {}ms", adjustment_ms),
167 effect: ActionEffect::LatencyAdjustment {
168 delta_ms: *adjustment_ms,
169 },
170 }),
171
172 BehaviorAction::TriggerChaosRule { rule_name } => Ok(ActionResult {
173 description: format!("Triggered chaos rule: {}", rule_name),
174 effect: ActionEffect::Chaostrigger {
175 rule_name: rule_name.clone(),
176 },
177 }),
178
179 BehaviorAction::ModifyResponseBody { path, value } => Ok(ActionResult {
180 description: format!("Modified response body at {} to {}", path, value),
181 effect: ActionEffect::BodyPatch {
182 path: path.clone(),
183 value: value.clone(),
184 },
185 }),
186 }
187 }
188
189 pub fn execute(&self, action: &BehaviorAction) -> Result<String> {
194 self.execute_action(action).map(|r| r.description)
195 }
196}
197
198impl Default for ActionExecutor {
199 fn default() -> Self {
200 Self::new()
201 }
202}
203
204#[cfg(test)]
205mod tests {
206 use super::*;
207
208 #[test]
209 fn test_noop_action() {
210 let executor = ActionExecutor::new();
211 let result = executor.execute(&BehaviorAction::NoOp).unwrap();
212 assert_eq!(result, "No operation");
213 }
214
215 #[test]
216 fn test_modify_conversion_rate() {
217 let executor = ActionExecutor::new();
218 let result = executor
219 .execute(&BehaviorAction::ModifyConversionRate { multiplier: 0.8 })
220 .unwrap();
221 assert!(result.contains("0.8"));
222 }
223
224 #[test]
225 fn test_decline_transaction() {
226 let executor = ActionExecutor::new();
227 let result = executor
228 .execute(&BehaviorAction::DeclineTransaction {
229 reason: "fraud_detected".to_string(),
230 })
231 .unwrap();
232 assert!(result.contains("fraud_detected"));
233 }
234
235 #[test]
236 fn test_execute_action_status_override() {
237 let executor = ActionExecutor::new();
238 let result = executor
239 .execute_action(&BehaviorAction::ChangeResponseStatus { status: 503 })
240 .unwrap();
241 assert_eq!(result.effect, ActionEffect::StatusOverride { status: 503 });
242 }
243
244 #[test]
245 fn test_execute_action_latency_adjustment() {
246 let executor = ActionExecutor::new();
247 let result = executor
248 .execute_action(&BehaviorAction::ModifyLatency { adjustment_ms: -50 })
249 .unwrap();
250 assert_eq!(result.effect, ActionEffect::LatencyAdjustment { delta_ms: -50 });
251 }
252
253 #[test]
254 fn test_execute_action_body_patch() {
255 let executor = ActionExecutor::new();
256 let result = executor
257 .execute_action(&BehaviorAction::ModifyResponseBody {
258 path: "$.price".to_string(),
259 value: "99.99".to_string(),
260 })
261 .unwrap();
262 assert_eq!(
263 result.effect,
264 ActionEffect::BodyPatch {
265 path: "$.price".to_string(),
266 value: "99.99".to_string(),
267 }
268 );
269 }
270
271 #[test]
272 fn test_execute_action_churn_multiplier() {
273 let executor = ActionExecutor::new();
274 let result = executor
275 .execute_action(&BehaviorAction::IncreaseChurnProbability { factor: 2.0 })
276 .unwrap();
277 assert_eq!(
278 result.effect,
279 ActionEffect::RateMultiplier {
280 target: "churn".to_string(),
281 multiplier: 2.0,
282 }
283 );
284 }
285
286 #[test]
287 fn test_execute_action_noop() {
288 let executor = ActionExecutor::new();
289 let result = executor.execute_action(&BehaviorAction::NoOp).unwrap();
290 assert_eq!(result.effect, ActionEffect::None);
291 }
292}