apm_core/wrapper/builtin/
mock_random.rs1use crate::config::resolve_outcome;
2use super::{load_transitions_with_outcomes, is_impl_mode, happy_script, sad_script, seed_from_ctx, write_and_spawn_script};
3use crate::wrapper::{Wrapper, WrapperContext};
4
5pub struct MockRandomWrapper;
6
7pub(crate) fn pick_transition_idx(seed: u64, count: usize) -> usize {
8 (seed as usize) % count
9}
10
11impl Wrapper for MockRandomWrapper {
12 fn spawn(&self, ctx: &WrapperContext) -> anyhow::Result<std::process::Child> {
13 let transitions = load_transitions_with_outcomes(ctx)?;
14 if transitions.is_empty() {
15 anyhow::bail!(
16 "mock-random: no valid transitions from state '{}'",
17 ctx.current_state
18 );
19 }
20 let seed = seed_from_ctx(ctx);
21 let idx = pick_transition_idx(seed, transitions.len());
22 let chosen = &transitions[idx];
23 let outcome = resolve_outcome(&chosen.0, &chosen.1);
24 let target = chosen.0.to.clone();
25 let script = if outcome == "success" {
26 happy_script(&ctx.ticket_id, &target, is_impl_mode(&transitions))
27 } else {
28 sad_script(&ctx.ticket_id, &target)
29 };
30 write_and_spawn_script("random", &script, ctx)
31 }
32}
33
34#[cfg(test)]
35mod tests {
36 use super::*;
37
38 #[test]
39 fn pick_transition_idx_is_deterministic_for_same_seed() {
40 let count = 5;
41 assert_eq!(pick_transition_idx(42, count), pick_transition_idx(42, count));
42 assert_eq!(pick_transition_idx(42, count), 42 % 5);
43 }
44
45 #[test]
46 fn pick_transition_idx_distributes_across_seeds() {
47 let count = 5;
49 let mut buckets = std::collections::HashSet::new();
50 for seed in 0u64..100 {
51 buckets.insert(pick_transition_idx(seed, count));
52 }
53 assert!(buckets.len() >= 3, "expected >=3 distinct outcomes across 100 seeds, got {}: {buckets:?}", buckets.len());
54 }
55
56 #[test]
57 fn pick_transition_idx_stays_in_bounds() {
58 for count in 1..=10 {
59 for seed in 0u64..50 {
60 let idx = pick_transition_idx(seed, count);
61 assert!(idx < count, "idx {idx} out of range for count {count} seed {seed}");
62 }
63 }
64 }
65}