1use super::JsonValue;
2use super::json_to_string;
3use super::session::{EffectRecord, RecordedOutcome};
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
6pub enum EffectReplayMode {
7 #[default]
8 Normal,
9 Record,
10 Replay,
11}
12
13#[derive(Debug, Clone, PartialEq, Eq)]
14pub enum ReplayFailure {
15 Exhausted {
16 effect_type: String,
17 position: usize,
18 },
19 Mismatch {
20 seq: u32,
21 expected: String,
22 got: String,
23 },
24 ArgsMismatch {
25 seq: u32,
26 effect_type: String,
27 expected: String,
28 got: String,
29 },
30 Unconsumed {
31 remaining: usize,
32 },
33}
34
35#[derive(Debug, Clone, Default)]
36pub struct EffectReplayState {
37 mode: EffectReplayMode,
38 recorded_effects: Vec<EffectRecord>,
39 replay_effects: Vec<EffectRecord>,
40 replay_pos: usize,
41 validate_replay_args: bool,
42 args_diff_count: usize,
43 group_stack: Vec<u32>,
45 branch_stack: Vec<u32>,
48 effect_count_stack: Vec<u32>,
50 next_group_id: u32,
52 group_consumed: Vec<usize>,
54 record_cap: Option<usize>,
60}
61
62impl EffectReplayState {
63 pub fn mode(&self) -> EffectReplayMode {
64 self.mode
65 }
66
67 pub fn set_normal(&mut self) {
68 self.mode = EffectReplayMode::Normal;
69 self.recorded_effects.clear();
70 self.replay_effects.clear();
71 self.replay_pos = 0;
72 self.validate_replay_args = false;
73 self.args_diff_count = 0;
74 self.reset_group_state();
75 }
76
77 pub fn start_recording(&mut self) {
78 self.mode = EffectReplayMode::Record;
79 self.recorded_effects.clear();
80 self.replay_effects.clear();
81 self.replay_pos = 0;
82 self.validate_replay_args = false;
83 self.args_diff_count = 0;
84 self.reset_group_state();
85 }
86
87 pub fn set_record_cap(&mut self, cap: Option<usize>) {
88 self.record_cap = cap;
89 }
90
91 pub fn record_full(&self) -> bool {
92 matches!(self.record_cap, Some(cap) if self.recorded_effects.len() >= cap)
93 }
94
95 pub fn start_replay(&mut self, effects: Vec<EffectRecord>, validate_args: bool) {
96 self.mode = EffectReplayMode::Replay;
97 self.replay_effects = effects;
98 self.replay_pos = 0;
99 self.validate_replay_args = validate_args;
100 self.recorded_effects.clear();
101 self.args_diff_count = 0;
102 self.reset_group_state();
103 }
104
105 pub fn take_recorded_effects(&mut self) -> Vec<EffectRecord> {
106 std::mem::take(&mut self.recorded_effects)
107 }
108
109 pub fn recorded_effects(&self) -> &[EffectRecord] {
110 &self.recorded_effects
111 }
112
113 pub fn replay_progress(&self) -> (usize, usize) {
114 (self.replay_pos, self.replay_effects.len())
115 }
116
117 pub fn args_diff_count(&self) -> usize {
118 self.args_diff_count
119 }
120
121 pub fn ensure_replay_consumed(&self) -> Result<(), ReplayFailure> {
122 if self.mode == EffectReplayMode::Replay && self.replay_pos < self.replay_effects.len() {
123 return Err(ReplayFailure::Unconsumed {
124 remaining: self.replay_effects.len() - self.replay_pos,
125 });
126 }
127 Ok(())
128 }
129
130 pub fn enter_group(&mut self) -> u32 {
132 self.next_group_id += 1;
133 let id = self.next_group_id;
134 self.group_stack.push(id);
135 self.branch_stack.push(0); self.effect_count_stack.push(0);
137 id
138 }
139
140 pub fn exit_group(&mut self) {
142 self.group_stack.pop();
143 self.branch_stack.pop();
144 self.effect_count_stack.pop();
145 }
146
147 pub fn set_branch(&mut self, index: u32) {
149 if let Some(last) = self.branch_stack.last_mut() {
150 *last = index;
151 }
152 if let Some(last) = self.effect_count_stack.last_mut() {
153 *last = 0;
154 }
155 }
156
157 pub fn record_effect(
158 &mut self,
159 effect_type: &str,
160 args: Vec<JsonValue>,
161 outcome: RecordedOutcome,
162 caller_fn: &str,
163 source_line: usize,
164 ) {
165 let seq = self.recorded_effects.len() as u32 + 1;
166 self.recorded_effects.push(EffectRecord {
167 seq,
168 effect_type: effect_type.to_string(),
169 args,
170 outcome,
171 caller_fn: caller_fn.to_string(),
172 source_line,
173 group_id: self.group_stack.last().copied(),
174 branch_path: if self.branch_stack.is_empty() {
175 None
176 } else {
177 Some(self.current_branch_path())
178 },
179 effect_occurrence: if self.branch_stack.is_empty() {
180 None
181 } else {
182 self.current_effect_occurrence()
183 },
184 });
185 self.bump_effect_occurrence();
186 }
187
188 pub fn replay_effect(
189 &mut self,
190 effect_type: &str,
191 got_args: Option<Vec<JsonValue>>,
192 ) -> Result<RecordedOutcome, ReplayFailure> {
193 if self.replay_pos < self.replay_effects.len()
196 && let Some(gid) = self.replay_effects[self.replay_pos].group_id
197 {
198 return self.replay_effect_in_group(gid, effect_type, got_args);
199 }
200
201 if self.replay_pos >= self.replay_effects.len() {
203 return Err(ReplayFailure::Exhausted {
204 effect_type: effect_type.to_string(),
205 position: self.replay_pos + 1,
206 });
207 }
208
209 let record = self.replay_effects[self.replay_pos].clone();
210 if record.effect_type != effect_type {
211 return Err(ReplayFailure::Mismatch {
212 seq: record.seq,
213 expected: record.effect_type,
214 got: effect_type.to_string(),
215 });
216 }
217
218 if let Some(got_args) = got_args
219 && got_args != record.args
220 {
221 if self.validate_replay_args {
222 return Err(ReplayFailure::ArgsMismatch {
223 seq: record.seq,
224 effect_type: effect_type.to_string(),
225 expected: json_to_string(&JsonValue::Array(record.args.clone())),
226 got: json_to_string(&JsonValue::Array(got_args)),
227 });
228 }
229 self.args_diff_count += 1;
230 }
231
232 self.replay_pos += 1;
233 Ok(record.outcome)
234 }
235
236 fn replay_effect_in_group(
239 &mut self,
240 group_id: u32,
241 effect_type: &str,
242 got_args: Option<Vec<JsonValue>>,
243 ) -> Result<RecordedOutcome, ReplayFailure> {
244 let group_start = self.replay_pos;
246 let group_end = self.replay_effects[group_start..]
247 .iter()
248 .position(|e| e.group_id != Some(group_id))
249 .map(|offset| group_start + offset)
250 .unwrap_or(self.replay_effects.len());
251
252 let current_bp = if self.branch_stack.is_empty() {
255 None
256 } else {
257 Some(self.current_branch_path())
258 };
259
260 let mut fallback_idx: Option<usize> = None;
261 for idx in group_start..group_end {
262 if self.group_consumed.contains(&idx) {
263 continue;
264 }
265 let record = &self.replay_effects[idx];
266 if record.effect_type != effect_type {
267 continue;
268 }
269
270 let args_ok = match (&got_args, self.validate_replay_args) {
272 (Some(got), true) if *got != record.args => false,
273 (Some(got), false) if *got != record.args => {
274 self.args_diff_count += 1;
275 true
276 }
277 _ => true,
278 };
279 if !args_ok {
280 continue;
281 }
282
283 let bp_match = match (¤t_bp, &record.branch_path) {
286 (Some(got), Some(rec)) => {
287 if got != rec {
288 continue; }
290 true
291 }
292 _ => false, };
294 if bp_match {
295 let current_occ = self.current_effect_occurrence();
297 match (current_occ, record.effect_occurrence) {
298 (Some(got), Some(rec)) if got == rec => {
299 return self.consume_group_match(idx, group_start, group_end);
300 }
301 (Some(_), Some(_)) => continue, _ => {
303 if fallback_idx.is_none() {
305 fallback_idx = Some(idx);
306 }
307 }
308 }
309 } else if fallback_idx.is_none() {
310 fallback_idx = Some(idx);
311 }
312 }
313
314 if let Some(idx) = fallback_idx {
316 return self.consume_group_match(idx, group_start, group_end);
317 }
318
319 Err(ReplayFailure::Mismatch {
321 seq: self.replay_effects[group_start].seq,
322 expected: format!("one of group {} effects", group_id),
323 got: effect_type.to_string(),
324 })
325 }
326
327 fn consume_group_match(
328 &mut self,
329 idx: usize,
330 group_start: usize,
331 group_end: usize,
332 ) -> Result<RecordedOutcome, ReplayFailure> {
333 let outcome = self.replay_effects[idx].outcome.clone();
334 self.bump_effect_occurrence();
335 self.group_consumed.push(idx);
336 let group_size = group_end - group_start;
337 if self.group_consumed.len() >= group_size {
338 self.replay_pos = group_end;
339 self.group_consumed.clear();
340 }
341 Ok(outcome)
342 }
343
344 fn reset_group_state(&mut self) {
345 self.group_stack.clear();
346 self.branch_stack.clear();
347 self.effect_count_stack.clear();
348 self.next_group_id = 0;
349 self.group_consumed.clear();
350 }
351
352 fn current_branch_path(&self) -> String {
353 self.branch_stack
354 .iter()
355 .map(|i| i.to_string())
356 .collect::<Vec<_>>()
357 .join(".")
358 }
359
360 fn current_effect_occurrence(&self) -> Option<u32> {
361 self.effect_count_stack.last().copied()
362 }
363
364 fn bump_effect_occurrence(&mut self) {
365 if let Some(last) = self.effect_count_stack.last_mut() {
366 *last += 1;
367 }
368 }
369}
370
371#[cfg(test)]
372mod tests {
373 use super::*;
374
375 fn recorded_value(text: &str) -> RecordedOutcome {
376 RecordedOutcome::Value(JsonValue::String(text.to_string()))
377 }
378
379 #[test]
380 fn nested_groups_preserve_outer_effect_occurrence() {
381 let mut state = EffectReplayState::default();
382
383 state.start_recording();
384 state.enter_group();
385 state.set_branch(0);
386 state.record_effect(
387 "Console.print",
388 vec![],
389 RecordedOutcome::Value(JsonValue::Null),
390 "",
391 0,
392 );
393
394 state.enter_group();
395 state.set_branch(1);
396 state.record_effect(
397 "Console.print",
398 vec![],
399 RecordedOutcome::Value(JsonValue::Null),
400 "",
401 0,
402 );
403 state.exit_group();
404
405 state.record_effect(
406 "Console.print",
407 vec![],
408 RecordedOutcome::Value(JsonValue::Null),
409 "",
410 0,
411 );
412
413 let effects = state.take_recorded_effects();
414 assert_eq!(effects.len(), 3);
415 assert_eq!(effects[0].branch_path.as_deref(), Some("0"));
416 assert_eq!(effects[0].effect_occurrence, Some(0));
417 assert_eq!(effects[1].branch_path.as_deref(), Some("0.1"));
418 assert_eq!(effects[1].effect_occurrence, Some(0));
419 assert_eq!(effects[2].branch_path.as_deref(), Some("0"));
420 assert_eq!(effects[2].effect_occurrence, Some(1));
421 }
422
423 #[test]
424 fn start_replay_clears_group_state() {
425 let mut state = EffectReplayState::default();
426 state.start_recording();
427 state.enter_group();
428 state.set_branch(3);
429 state.record_effect(
430 "Console.print",
431 vec![],
432 RecordedOutcome::Value(JsonValue::Null),
433 "",
434 0,
435 );
436
437 state.start_replay(Vec::new(), true);
438
439 assert!(state.group_stack.is_empty());
440 assert!(state.branch_stack.is_empty());
441 assert!(state.effect_count_stack.is_empty());
442 assert!(state.group_consumed.is_empty());
443 assert_eq!(state.next_group_id, 0);
444 assert_eq!(state.args_diff_count, 0);
445 }
446
447 #[test]
448 fn replay_group_matching_uses_effect_occurrence() {
449 let mut state = EffectReplayState::default();
450 state.start_replay(
451 vec![
452 EffectRecord {
453 seq: 1,
454 effect_type: "Console.print".to_string(),
455 args: vec![JsonValue::String("same".to_string())],
456 outcome: recorded_value("first"),
457 caller_fn: String::new(),
458 source_line: 0,
459 group_id: Some(1),
460 branch_path: Some("0".to_string()),
461 effect_occurrence: Some(0),
462 },
463 EffectRecord {
464 seq: 2,
465 effect_type: "Console.print".to_string(),
466 args: vec![JsonValue::String("same".to_string())],
467 outcome: recorded_value("second"),
468 caller_fn: String::new(),
469 source_line: 0,
470 group_id: Some(1),
471 branch_path: Some("0".to_string()),
472 effect_occurrence: Some(1),
473 },
474 ],
475 true,
476 );
477
478 state.enter_group();
479 state.set_branch(0);
480
481 let first = state
482 .replay_effect(
483 "Console.print",
484 Some(vec![JsonValue::String("same".to_string())]),
485 )
486 .expect("first replay should match");
487 let second = state
488 .replay_effect(
489 "Console.print",
490 Some(vec![JsonValue::String("same".to_string())]),
491 )
492 .expect("second replay should match");
493
494 assert_eq!(first, recorded_value("first"));
495 assert_eq!(second, recorded_value("second"));
496 }
497}