1use crate::context::ContextKey;
9use crate::fact::ProposedFact;
10
11#[derive(Debug, Default)]
17pub struct AgentEffect {
18 pub proposals: Vec<ProposedFact>,
20}
21
22impl AgentEffect {
23 #[must_use]
25 pub fn empty() -> Self {
26 Self::default()
27 }
28
29 #[must_use]
31 pub fn with_proposal(proposal: ProposedFact) -> Self {
32 Self {
33 proposals: vec![proposal],
34 }
35 }
36
37 #[must_use]
39 pub fn with_proposals(proposals: Vec<ProposedFact>) -> Self {
40 Self { proposals }
41 }
42
43 #[must_use]
45 pub fn is_empty(&self) -> bool {
46 self.proposals.is_empty()
47 }
48
49 #[must_use]
51 pub fn affected_keys(&self) -> Vec<ContextKey> {
52 let mut keys: Vec<ContextKey> = self.proposals.iter().map(|p| p.key).collect();
53 keys.sort();
54 keys.dedup();
55 keys
56 }
57}
58
59#[cfg(test)]
60mod tests {
61 use super::*;
62
63 fn proposal(key: ContextKey, id: &str) -> ProposedFact {
64 ProposedFact::new(key, id, "content", "test")
65 }
66
67 #[test]
68 fn empty_effect_is_empty() {
69 let e = AgentEffect::empty();
70 assert!(e.is_empty());
71 assert!(e.proposals.is_empty());
72 }
73
74 #[test]
75 fn with_proposal_single() {
76 let e = AgentEffect::with_proposal(proposal(ContextKey::Seeds, "p1"));
77 assert!(!e.is_empty());
78 assert_eq!(e.proposals.len(), 1);
79 assert_eq!(e.proposals[0].id, "p1");
80 }
81
82 #[test]
83 fn with_proposals_multiple() {
84 let e = AgentEffect::with_proposals(vec![
85 proposal(ContextKey::Seeds, "p1"),
86 proposal(ContextKey::Hypotheses, "p2"),
87 ]);
88 assert_eq!(e.proposals.len(), 2);
89 }
90
91 #[test]
92 fn is_empty_false_for_nonempty() {
93 let e = AgentEffect::with_proposal(proposal(ContextKey::Signals, "x"));
94 assert!(!e.is_empty());
95 }
96
97 #[test]
98 fn affected_keys_deduplicates_and_sorts() {
99 let e = AgentEffect::with_proposals(vec![
100 proposal(ContextKey::Signals, "a"),
101 proposal(ContextKey::Seeds, "b"),
102 proposal(ContextKey::Signals, "c"),
103 proposal(ContextKey::Seeds, "d"),
104 proposal(ContextKey::Hypotheses, "e"),
105 ]);
106 let keys = e.affected_keys();
107 assert_eq!(keys.len(), 3);
108 for window in keys.windows(2) {
110 assert!(window[0] <= window[1]);
111 }
112 let mut deduped = keys.clone();
114 deduped.dedup();
115 assert_eq!(keys, deduped);
116 }
117
118 #[test]
119 fn affected_keys_empty_for_empty_effect() {
120 let e = AgentEffect::empty();
121 assert!(e.affected_keys().is_empty());
122 }
123
124 mod prop {
125 use super::*;
126 use proptest::prelude::*;
127
128 fn arb_context_key() -> impl Strategy<Value = ContextKey> {
129 prop_oneof![
130 Just(ContextKey::Seeds),
131 Just(ContextKey::Hypotheses),
132 Just(ContextKey::Strategies),
133 Just(ContextKey::Constraints),
134 Just(ContextKey::Signals),
135 Just(ContextKey::Competitors),
136 Just(ContextKey::Evaluations),
137 Just(ContextKey::Proposals),
138 Just(ContextKey::Diagnostic),
139 ]
140 }
141
142 proptest! {
143 #[test]
144 fn affected_keys_never_has_duplicates(
145 keys in proptest::collection::vec(arb_context_key(), 0..50),
146 ) {
147 let proposals: Vec<ProposedFact> = keys
148 .iter()
149 .enumerate()
150 .map(|(i, &k)| ProposedFact::new(k, format!("p{i}"), "c", "prov"))
151 .collect();
152 let effect = AgentEffect::with_proposals(proposals);
153 let result = effect.affected_keys();
154 let mut deduped = result.clone();
155 deduped.dedup();
156 prop_assert_eq!(result, deduped);
157 }
158 }
159 }
160}