Skip to main content

vision_calibration_pipeline/session/
calibsession.rs

1//! Calibration session container with mutable state.
2//!
3//! Provides a generic session container parameterized over a problem type.
4//! Sessions store configuration, input data, intermediate state, and the
5//! final output. Step functions mutate the session in-place.
6
7use crate::Error;
8use serde::{Deserialize, Serialize};
9
10use super::problem_type::ProblemType;
11use super::types::{ExportRecord, LogEntry, SessionMetadata};
12
13/// A calibration session container with mutable state.
14///
15/// The session stores configuration, input data, intermediate state,
16/// and the final output. Step functions mutate the session in-place.
17///
18/// # Design Principles
19///
20/// - **Single output**: Only one final result is stored.
21/// - **Embedded input**: Input data is stored directly in the session.
22/// - **Config changes don't auto-clear**: Changing config doesn't invalidate output.
23/// - **Exports collection**: Multiple exports can be generated and stored.
24///
25/// # Example
26///
27/// ```no_run
28/// use vision_calibration_pipeline::session::CalibrationSession;
29/// use vision_calibration_pipeline::planar_intrinsics::{PlanarIntrinsicsProblem, step_init, step_optimize};
30/// # fn main() -> anyhow::Result<()> {
31/// # let dataset = unimplemented!();
32///
33/// let mut session = CalibrationSession::<PlanarIntrinsicsProblem>::new();
34/// session.set_input(dataset)?;
35///
36/// step_init(&mut session, None)?;
37/// step_optimize(&mut session, None)?;
38///
39/// let export = session.export()?;
40/// # Ok(())
41/// # }
42/// ```
43#[derive(Debug, Clone, Serialize, Deserialize)]
44#[serde(bound = "P: ProblemType")]
45pub struct CalibrationSession<P: ProblemType> {
46    /// Session metadata (problem type, schema version, timestamps, description).
47    pub metadata: SessionMetadata,
48
49    /// Configuration parameters (always present, defaults if not explicitly set).
50    pub config: P::Config,
51
52    /// Input observations (embedded). `None` until set.
53    input: Option<P::Input>,
54
55    /// Problem-specific intermediate state (default until computed).
56    pub state: P::State,
57
58    /// Final calibration output. `None` until computed.
59    output: Option<P::Output>,
60
61    /// Collection of generated exports.
62    pub exports: Vec<ExportRecord<P::Export>>,
63
64    /// Operation log (lightweight audit trail).
65    pub log: Vec<LogEntry>,
66}
67
68impl<P: ProblemType> CalibrationSession<P> {
69    // ─────────────────────────────────────────────────────────────────────────
70    // Construction
71    // ─────────────────────────────────────────────────────────────────────────
72
73    /// Create a new empty session with default configuration.
74    pub fn new() -> Self {
75        Self {
76            metadata: SessionMetadata::new(P::name(), P::schema_version()),
77            config: P::Config::default(),
78            input: None,
79            state: P::State::default(),
80            output: None,
81            exports: Vec::new(),
82            log: Vec::new(),
83        }
84    }
85
86    /// Create a new session with a description.
87    pub fn with_description(description: impl Into<String>) -> Self {
88        Self {
89            metadata: SessionMetadata::with_description(
90                P::name(),
91                P::schema_version(),
92                description,
93            ),
94            config: P::Config::default(),
95            input: None,
96            state: P::State::default(),
97            output: None,
98            exports: Vec::new(),
99            log: Vec::new(),
100        }
101    }
102
103    /// Create a new session with input data.
104    ///
105    /// # Errors
106    ///
107    /// Returns an error if input validation fails.
108    pub fn with_input(input: P::Input) -> Result<Self, Error> {
109        let mut session = Self::new();
110        session.set_input(input)?;
111        Ok(session)
112    }
113
114    // ─────────────────────────────────────────────────────────────────────────
115    // Input Management
116    // ─────────────────────────────────────────────────────────────────────────
117
118    /// Set input data, applying validation and invalidation policy.
119    ///
120    /// # Errors
121    ///
122    /// Returns an error if [`ProblemType::validate_input`] fails.
123    pub fn set_input(&mut self, input: P::Input) -> Result<(), Error> {
124        P::validate_input(&input)?;
125
126        let policy = P::on_input_change();
127        if policy.clear_state {
128            self.state = P::State::default();
129        }
130        if policy.clear_output {
131            self.output = None;
132        }
133        if policy.clear_exports {
134            self.exports.clear();
135        }
136
137        self.input = Some(input);
138        self.metadata.touch();
139        Ok(())
140    }
141
142    /// Get a reference to the input, if set.
143    pub fn input(&self) -> Option<&P::Input> {
144        self.input.as_ref()
145    }
146
147    /// Get a mutable reference to the input, if set.
148    pub fn input_mut(&mut self) -> Option<&mut P::Input> {
149        self.input.as_mut()
150    }
151
152    /// Get a reference to the input, or error if not set.
153    ///
154    /// # Errors
155    ///
156    /// Returns an error if input is not set.
157    pub fn require_input(&self) -> Result<&P::Input, Error> {
158        self.input
159            .as_ref()
160            .ok_or_else(|| Error::not_available("input"))
161    }
162
163    /// Get a mutable reference to the input, or error if not set.
164    ///
165    /// # Errors
166    ///
167    /// Returns an error if input is not set.
168    pub fn require_input_mut(&mut self) -> Result<&mut P::Input, Error> {
169        self.input
170            .as_mut()
171            .ok_or_else(|| Error::not_available("input"))
172    }
173
174    /// Check if input is set.
175    pub fn has_input(&self) -> bool {
176        self.input.is_some()
177    }
178
179    /// Clear input data, applying invalidation policy.
180    pub fn clear_input(&mut self) {
181        let policy = P::on_input_change();
182        if policy.clear_state {
183            self.state = P::State::default();
184        }
185        if policy.clear_output {
186            self.output = None;
187        }
188        if policy.clear_exports {
189            self.exports.clear();
190        }
191        self.input = None;
192        self.metadata.touch();
193    }
194
195    // ─────────────────────────────────────────────────────────────────────────
196    // Configuration Management
197    // ─────────────────────────────────────────────────────────────────────────
198
199    /// Set configuration, applying validation and invalidation policy.
200    ///
201    /// # Errors
202    ///
203    /// Returns an error if [`ProblemType::validate_config`] fails.
204    pub fn set_config(&mut self, config: P::Config) -> Result<(), Error> {
205        P::validate_config(&config)?;
206
207        let policy = P::on_config_change();
208        if policy.clear_state {
209            self.state = P::State::default();
210        }
211        if policy.clear_output {
212            self.output = None;
213        }
214        if policy.clear_exports {
215            self.exports.clear();
216        }
217
218        self.config = config;
219        self.metadata.touch();
220        Ok(())
221    }
222
223    /// Update configuration with a closure.
224    ///
225    /// The closure receives a mutable reference to the current config.
226    /// After the closure returns, validation and invalidation policy are applied.
227    ///
228    /// # Errors
229    ///
230    /// Returns an error if validation fails after the update.
231    pub fn update_config<F>(&mut self, f: F) -> Result<(), Error>
232    where
233        F: FnOnce(&mut P::Config),
234    {
235        let mut new_config = self.config.clone();
236        f(&mut new_config);
237        self.set_config(new_config)
238    }
239
240    // ─────────────────────────────────────────────────────────────────────────
241    // Output Management
242    // ─────────────────────────────────────────────────────────────────────────
243
244    /// Get a reference to the output, if computed.
245    pub fn output(&self) -> Option<&P::Output> {
246        self.output.as_ref()
247    }
248
249    /// Get a mutable reference to the output, if computed.
250    pub fn output_mut(&mut self) -> Option<&mut P::Output> {
251        self.output.as_mut()
252    }
253
254    /// Get a reference to the output, or error if not computed.
255    ///
256    /// # Errors
257    ///
258    /// Returns an error if output is not computed.
259    pub fn require_output(&self) -> Result<&P::Output, Error> {
260        self.output
261            .as_ref()
262            .ok_or_else(|| Error::not_available("output"))
263    }
264
265    /// Set the output (typically called by step functions).
266    pub fn set_output(&mut self, output: P::Output) {
267        self.output = Some(output);
268        self.metadata.touch();
269    }
270
271    /// Check if output is computed.
272    pub fn has_output(&self) -> bool {
273        self.output.is_some()
274    }
275
276    /// Clear output.
277    pub fn clear_output(&mut self) {
278        self.output = None;
279        self.metadata.touch();
280    }
281
282    // ─────────────────────────────────────────────────────────────────────────
283    // Export
284    // ─────────────────────────────────────────────────────────────────────────
285
286    /// Export the current output and add to exports collection.
287    ///
288    /// # Errors
289    ///
290    /// Returns an error if output is not computed or if export conversion fails.
291    pub fn export(&mut self) -> Result<P::Export, Error> {
292        let output = self.require_output()?;
293        let export = P::export(output, &self.config)?;
294        self.exports.push(ExportRecord::new(export.clone()));
295        self.metadata.touch();
296        Ok(export)
297    }
298
299    /// Export the current output with notes and add to exports collection.
300    ///
301    /// # Errors
302    ///
303    /// Returns an error if output is not computed or if export conversion fails.
304    pub fn export_with_notes(&mut self, notes: impl Into<String>) -> Result<P::Export, Error> {
305        let output = self.require_output()?;
306        let export = P::export(output, &self.config)?;
307        self.exports
308            .push(ExportRecord::with_notes(export.clone(), notes));
309        self.metadata.touch();
310        Ok(export)
311    }
312
313    /// Export without adding to collection (peek).
314    ///
315    /// Useful for inspecting the export without modifying the session.
316    ///
317    /// # Errors
318    ///
319    /// Returns an error if output is not computed or if export conversion fails.
320    pub fn export_peek(&self) -> Result<P::Export, Error> {
321        let output = self.require_output()?;
322        P::export(output, &self.config)
323    }
324
325    // ─────────────────────────────────────────────────────────────────────────
326    // Validation
327    // ─────────────────────────────────────────────────────────────────────────
328
329    /// Validate that the session is ready for processing.
330    ///
331    /// Checks:
332    /// 1. Input is set and valid
333    /// 2. Config is valid
334    /// 3. Input and config are compatible (cross-validation)
335    ///
336    /// # Errors
337    ///
338    /// Returns an error if any validation check fails.
339    pub fn validate(&self) -> Result<(), Error> {
340        let input = self.require_input()?;
341        P::validate_input(input)?;
342        P::validate_config(&self.config)?;
343        P::validate_input_config(input, &self.config)?;
344        Ok(())
345    }
346
347    // ─────────────────────────────────────────────────────────────────────────
348    // Logging
349    // ─────────────────────────────────────────────────────────────────────────
350
351    /// Log a successful operation.
352    pub fn log_success(&mut self, operation: impl Into<String>) {
353        self.log.push(LogEntry::success(operation));
354        self.metadata.touch();
355    }
356
357    /// Log a successful operation with notes.
358    pub fn log_success_with_notes(
359        &mut self,
360        operation: impl Into<String>,
361        notes: impl Into<String>,
362    ) {
363        self.log
364            .push(LogEntry::success_with_notes(operation, notes));
365        self.metadata.touch();
366    }
367
368    /// Log a failed operation.
369    pub fn log_failure(&mut self, operation: impl Into<String>, error: impl Into<String>) {
370        self.log.push(LogEntry::failure(operation, error));
371        self.metadata.touch();
372    }
373
374    // ─────────────────────────────────────────────────────────────────────────
375    // Reset
376    // ─────────────────────────────────────────────────────────────────────────
377
378    /// Reset state to default, keeping input, config, and output.
379    pub fn reset_state(&mut self) {
380        self.state = P::State::default();
381        self.metadata.touch();
382    }
383
384    /// Reset output, keeping input, config, and state.
385    pub fn reset_output(&mut self) {
386        self.output = None;
387        self.metadata.touch();
388    }
389
390    /// Reset everything except config and description.
391    pub fn reset(&mut self) {
392        self.input = None;
393        self.state = P::State::default();
394        self.output = None;
395        self.exports.clear();
396        self.log.clear();
397        self.metadata.touch();
398    }
399
400    // ─────────────────────────────────────────────────────────────────────────
401    // Serialization
402    // ─────────────────────────────────────────────────────────────────────────
403
404    /// Serialize session to JSON string.
405    ///
406    /// # Errors
407    ///
408    /// Returns an error if serialization fails.
409    pub fn to_json(&self) -> Result<String, Error> {
410        // Pin metadata identity/version to the problem type contract on write.
411        let mut value = serde_json::to_value(self).map_err(Error::Serde)?;
412        let metadata = value
413            .get_mut("metadata")
414            .and_then(serde_json::Value::as_object_mut)
415            .ok_or_else(|| Error::numerical("session metadata missing during serialization"))?;
416        metadata.insert(
417            "problem_type".to_string(),
418            serde_json::Value::String(P::name().to_string()),
419        );
420        metadata.insert(
421            "schema_version".to_string(),
422            serde_json::Value::Number(P::schema_version().into()),
423        );
424        serde_json::to_string_pretty(&value).map_err(Error::Serde)
425    }
426
427    /// Deserialize session from JSON string.
428    ///
429    /// # Errors
430    ///
431    /// Returns an error if:
432    /// - Deserialization fails
433    /// - Problem type does not match `P::name()`
434    /// - Schema version does not match `P::schema_version()`
435    pub fn from_json(json: &str) -> Result<Self, Error> {
436        let session: Self = serde_json::from_str(json)?;
437        let expected_problem_type = P::name();
438        let expected_schema_version = P::schema_version();
439
440        if session.metadata.problem_type != expected_problem_type {
441            return Err(Error::invalid_input(format!(
442                "session problem_type '{}' does not match expected '{}'",
443                session.metadata.problem_type, expected_problem_type
444            )));
445        }
446
447        if session.metadata.schema_version != expected_schema_version {
448            return Err(Error::invalid_input(format!(
449                "session schema version {} does not match expected version {}",
450                session.metadata.schema_version, expected_schema_version
451            )));
452        }
453
454        Ok(session)
455    }
456}
457
458impl<P: ProblemType> Default for CalibrationSession<P> {
459    fn default() -> Self {
460        Self::new()
461    }
462}
463
464#[cfg(test)]
465mod tests {
466    use super::*;
467    use serde::{Deserialize, Serialize};
468
469    // ─────────────────────────────────────────────────────────────────────────
470    // Mock Problem Type for Testing
471    // ─────────────────────────────────────────────────────────────────────────
472
473    #[derive(Clone, Debug, Default, Serialize, Deserialize)]
474    struct MockConfig {
475        scale: f64,
476        max_iters: usize,
477    }
478
479    #[derive(Clone, Debug, Serialize, Deserialize)]
480    struct MockInput {
481        data: Vec<f64>,
482    }
483
484    #[derive(Clone, Debug, Default, Serialize, Deserialize)]
485    struct MockState {
486        computed: Option<f64>,
487    }
488
489    #[derive(Clone, Debug, Serialize, Deserialize)]
490    struct MockOutput {
491        result: f64,
492    }
493
494    #[derive(Clone, Debug, Serialize, Deserialize)]
495    struct MockExport {
496        value: f64,
497    }
498
499    #[derive(Debug)]
500    struct MockProblem;
501
502    impl ProblemType for MockProblem {
503        type Config = MockConfig;
504        type Input = MockInput;
505        type State = MockState;
506        type Output = MockOutput;
507        type Export = MockExport;
508
509        fn name() -> &'static str {
510            "mock_problem"
511        }
512
513        fn schema_version() -> u32 {
514            1
515        }
516
517        fn validate_input(input: &Self::Input) -> Result<(), Error> {
518            if input.data.is_empty() {
519                return Err(Error::invalid_input("input data cannot be empty"));
520            }
521            Ok(())
522        }
523
524        fn validate_config(config: &Self::Config) -> Result<(), Error> {
525            if config.max_iters == 0 {
526                return Err(Error::invalid_input("max_iters must be positive"));
527            }
528            Ok(())
529        }
530
531        fn export(output: &Self::Output, _config: &Self::Config) -> Result<Self::Export, Error> {
532            Ok(MockExport {
533                value: output.result,
534            })
535        }
536    }
537
538    // ─────────────────────────────────────────────────────────────────────────
539    // Tests
540    // ─────────────────────────────────────────────────────────────────────────
541
542    #[test]
543    fn new_session_has_defaults() {
544        let session = CalibrationSession::<MockProblem>::new();
545        assert_eq!(session.metadata.problem_type, "mock_problem");
546        assert_eq!(session.metadata.schema_version, 1);
547        assert!(session.metadata.description.is_none());
548        assert!(session.input().is_none());
549        assert!(session.output().is_none());
550        assert!(session.exports.is_empty());
551        assert!(session.log.is_empty());
552    }
553
554    #[test]
555    fn with_description() {
556        let session =
557            CalibrationSession::<MockProblem>::with_description("Test calibration session");
558        assert_eq!(
559            session.metadata.description,
560            Some("Test calibration session".to_string())
561        );
562    }
563
564    #[test]
565    fn set_input_validates() {
566        let mut session = CalibrationSession::<MockProblem>::new();
567
568        // Empty input should fail
569        let result = session.set_input(MockInput { data: vec![] });
570        assert!(result.is_err());
571        assert!(result.unwrap_err().to_string().contains("empty"));
572
573        // Valid input should succeed
574        let result = session.set_input(MockInput {
575            data: vec![1.0, 2.0],
576        });
577        assert!(result.is_ok());
578        assert!(session.has_input());
579    }
580
581    #[test]
582    fn set_input_clears_state_and_output() {
583        let mut session = CalibrationSession::<MockProblem>::new();
584        session.set_input(MockInput { data: vec![1.0] }).unwrap();
585        session.state.computed = Some(42.0);
586        session.set_output(MockOutput { result: 100.0 });
587
588        // Set new input - should clear state and output
589        session.set_input(MockInput { data: vec![2.0] }).unwrap();
590
591        assert!(session.state.computed.is_none());
592        assert!(session.output().is_none());
593    }
594
595    #[test]
596    fn set_config_keeps_output() {
597        let mut session = CalibrationSession::<MockProblem>::new();
598        session.set_input(MockInput { data: vec![1.0] }).unwrap();
599        session.set_output(MockOutput { result: 100.0 });
600
601        // Change config - should keep output (per default policy)
602        session
603            .set_config(MockConfig {
604                scale: 2.0,
605                max_iters: 100,
606            })
607            .unwrap();
608
609        assert!(session.has_output());
610        assert_eq!(session.output().unwrap().result, 100.0);
611    }
612
613    #[test]
614    fn set_config_validates() {
615        let mut session = CalibrationSession::<MockProblem>::new();
616
617        // Invalid config should fail
618        let result = session.set_config(MockConfig {
619            scale: 1.0,
620            max_iters: 0,
621        });
622        assert!(result.is_err());
623        assert!(result.unwrap_err().to_string().contains("max_iters"));
624    }
625
626    #[test]
627    fn update_config() {
628        let mut session = CalibrationSession::<MockProblem>::new();
629        session.config.scale = 1.0;
630        session.config.max_iters = 10;
631
632        session.update_config(|c| c.scale = 5.0).unwrap();
633
634        assert_eq!(session.config.scale, 5.0);
635        assert_eq!(session.config.max_iters, 10); // unchanged
636    }
637
638    #[test]
639    fn require_input_errors_when_none() {
640        let session = CalibrationSession::<MockProblem>::new();
641        let result = session.require_input();
642        assert!(result.is_err());
643        assert!(result.unwrap_err().to_string().contains("input"));
644    }
645
646    #[test]
647    fn require_output_errors_when_none() {
648        let session = CalibrationSession::<MockProblem>::new();
649        let result = session.require_output();
650        assert!(result.is_err());
651        assert!(result.unwrap_err().to_string().contains("output"));
652    }
653
654    #[test]
655    fn export_adds_to_collection() {
656        let mut session = CalibrationSession::<MockProblem>::new();
657        session.set_input(MockInput { data: vec![1.0] }).unwrap();
658        session.set_output(MockOutput { result: 42.0 });
659
660        assert!(session.exports.is_empty());
661
662        let export1 = session.export().unwrap();
663        assert_eq!(export1.value, 42.0);
664        assert_eq!(session.exports.len(), 1);
665
666        let export2 = session.export().unwrap();
667        assert_eq!(export2.value, 42.0);
668        assert_eq!(session.exports.len(), 2);
669    }
670
671    #[test]
672    fn export_peek_does_not_store() {
673        let mut session = CalibrationSession::<MockProblem>::new();
674        session.set_input(MockInput { data: vec![1.0] }).unwrap();
675        session.set_output(MockOutput { result: 42.0 });
676
677        let export = session.export_peek().unwrap();
678        assert_eq!(export.value, 42.0);
679        assert!(session.exports.is_empty()); // Not added
680    }
681
682    #[test]
683    fn export_with_notes() {
684        let mut session = CalibrationSession::<MockProblem>::new();
685        session.set_input(MockInput { data: vec![1.0] }).unwrap();
686        session.set_output(MockOutput { result: 42.0 });
687
688        session.export_with_notes("final result").unwrap();
689
690        assert_eq!(session.exports.len(), 1);
691        assert_eq!(session.exports[0].notes, Some("final result".to_string()));
692    }
693
694    #[test]
695    fn validate_checks_all_hooks() {
696        let mut session = CalibrationSession::<MockProblem>::new();
697
698        // No input - should fail
699        let result = session.validate();
700        assert!(result.is_err());
701
702        // With valid input and config
703        session.set_input(MockInput { data: vec![1.0] }).unwrap();
704        session.config.max_iters = 10;
705        let result = session.validate();
706        assert!(result.is_ok());
707    }
708
709    #[test]
710    fn log_entries_recorded() {
711        let mut session = CalibrationSession::<MockProblem>::new();
712
713        session.log_success("init");
714        session.log_success_with_notes("optimize", "converged");
715        session.log_failure("filter", "too few points");
716
717        assert_eq!(session.log.len(), 3);
718        assert!(session.log[0].success);
719        assert_eq!(session.log[0].operation, "init");
720        assert!(session.log[1].success);
721        assert_eq!(session.log[1].notes, Some("converged".to_string()));
722        assert!(!session.log[2].success);
723        assert_eq!(session.log[2].notes, Some("too few points".to_string()));
724    }
725
726    #[test]
727    fn json_roundtrip_empty() {
728        let session = CalibrationSession::<MockProblem>::new();
729        let json = session.to_json().unwrap();
730        let restored = CalibrationSession::<MockProblem>::from_json(&json).unwrap();
731
732        assert_eq!(restored.metadata.problem_type, "mock_problem");
733        assert!(restored.input().is_none());
734        assert!(restored.output().is_none());
735    }
736
737    #[test]
738    fn json_roundtrip_with_input() {
739        let mut session = CalibrationSession::<MockProblem>::new();
740        session
741            .set_input(MockInput {
742                data: vec![1.0, 2.0, 3.0],
743            })
744            .unwrap();
745
746        let json = session.to_json().unwrap();
747        let restored = CalibrationSession::<MockProblem>::from_json(&json).unwrap();
748
749        let input = restored.input().unwrap();
750        assert_eq!(input.data, vec![1.0, 2.0, 3.0]);
751    }
752
753    #[test]
754    fn json_roundtrip_with_output() {
755        let mut session = CalibrationSession::<MockProblem>::new();
756        session.set_input(MockInput { data: vec![1.0] }).unwrap();
757        session.set_output(MockOutput { result: 42.0 });
758
759        let json = session.to_json().unwrap();
760        let restored = CalibrationSession::<MockProblem>::from_json(&json).unwrap();
761
762        assert_eq!(restored.output().unwrap().result, 42.0);
763    }
764
765    #[test]
766    fn json_roundtrip_with_exports() {
767        let mut session = CalibrationSession::<MockProblem>::new();
768        session.set_input(MockInput { data: vec![1.0] }).unwrap();
769        session.set_output(MockOutput { result: 42.0 });
770        session.export().unwrap();
771        session.export_with_notes("second export").unwrap();
772
773        let json = session.to_json().unwrap();
774        let restored = CalibrationSession::<MockProblem>::from_json(&json).unwrap();
775
776        assert_eq!(restored.exports.len(), 2);
777        assert!(restored.exports[0].notes.is_none());
778        assert_eq!(restored.exports[1].notes, Some("second export".to_string()));
779    }
780
781    #[test]
782    fn schema_version_checked() {
783        // Create a session and serialize it.
784        let session = CalibrationSession::<MockProblem>::new();
785        let json = session.to_json().unwrap();
786
787        // Newer version should fail.
788        let newer = json.replace("\"schema_version\": 1", "\"schema_version\": 999");
789        let newer_result = CalibrationSession::<MockProblem>::from_json(&newer);
790        assert!(newer_result.is_err());
791        assert!(
792            newer_result
793                .unwrap_err()
794                .to_string()
795                .contains("schema version")
796        );
797
798        // Older version should also fail.
799        let older = json.replace("\"schema_version\": 1", "\"schema_version\": 0");
800        let older_result = CalibrationSession::<MockProblem>::from_json(&older);
801        assert!(older_result.is_err());
802        assert!(
803            older_result
804                .unwrap_err()
805                .to_string()
806                .contains("schema version")
807        );
808    }
809
810    #[test]
811    fn problem_type_checked() {
812        let session = CalibrationSession::<MockProblem>::new();
813        let json = session.to_json().unwrap();
814        let json = json.replace(
815            "\"problem_type\": \"mock_problem\"",
816            "\"problem_type\": \"other\"",
817        );
818
819        let result = CalibrationSession::<MockProblem>::from_json(&json);
820        assert!(result.is_err());
821        assert!(result.unwrap_err().to_string().contains("problem_type"));
822    }
823
824    #[test]
825    fn to_json_pins_problem_type_and_schema_version() {
826        let mut session = CalibrationSession::<MockProblem>::new();
827        session.metadata.problem_type = "tampered".to_string();
828        session.metadata.schema_version = 42;
829
830        let json = session.to_json().unwrap();
831        let restored: CalibrationSession<MockProblem> = serde_json::from_str(&json).unwrap();
832
833        assert_eq!(restored.metadata.problem_type, MockProblem::name());
834        assert_eq!(
835            restored.metadata.schema_version,
836            MockProblem::schema_version()
837        );
838    }
839
840    #[test]
841    fn reset_state() {
842        let mut session = CalibrationSession::<MockProblem>::new();
843        session.set_input(MockInput { data: vec![1.0] }).unwrap();
844        session.state.computed = Some(42.0);
845        session.set_output(MockOutput { result: 100.0 });
846
847        session.reset_state();
848
849        assert!(session.has_input()); // Kept
850        assert!(session.state.computed.is_none()); // Cleared
851        assert!(session.has_output()); // Kept
852    }
853
854    #[test]
855    fn reset_output() {
856        let mut session = CalibrationSession::<MockProblem>::new();
857        session.set_input(MockInput { data: vec![1.0] }).unwrap();
858        session.state.computed = Some(42.0);
859        session.set_output(MockOutput { result: 100.0 });
860
861        session.reset_output();
862
863        assert!(session.has_input()); // Kept
864        assert!(session.state.computed.is_some()); // Kept
865        assert!(!session.has_output()); // Cleared
866    }
867
868    #[test]
869    fn reset_all() {
870        let mut session = CalibrationSession::<MockProblem>::new();
871        session.set_input(MockInput { data: vec![1.0] }).unwrap();
872        session.state.computed = Some(42.0);
873        session.set_output(MockOutput { result: 100.0 });
874        session.export().unwrap();
875        session.log_success("test");
876
877        session.reset();
878
879        assert!(!session.has_input());
880        assert!(session.state.computed.is_none());
881        assert!(!session.has_output());
882        assert!(session.exports.is_empty());
883        assert!(session.log.is_empty());
884    }
885
886    #[test]
887    fn clear_input() {
888        let mut session = CalibrationSession::<MockProblem>::new();
889        session.set_input(MockInput { data: vec![1.0] }).unwrap();
890        session.state.computed = Some(42.0);
891        session.set_output(MockOutput { result: 100.0 });
892
893        session.clear_input();
894
895        assert!(!session.has_input());
896        assert!(session.state.computed.is_none()); // Cleared per policy
897        assert!(!session.has_output()); // Cleared per policy
898    }
899
900    #[test]
901    fn with_input_validates() {
902        // Invalid input
903        let result = CalibrationSession::<MockProblem>::with_input(MockInput { data: vec![] });
904        assert!(result.is_err());
905
906        // Valid input
907        let result = CalibrationSession::<MockProblem>::with_input(MockInput { data: vec![1.0] });
908        assert!(result.is_ok());
909        assert!(result.unwrap().has_input());
910    }
911
912    #[test]
913    fn metadata_timestamps_update() {
914        let mut session = CalibrationSession::<MockProblem>::new();
915        let created = session.metadata.created_at;
916        let initial_modified = session.metadata.last_modified;
917
918        // Operations should update last_modified
919        session.set_input(MockInput { data: vec![1.0] }).unwrap();
920
921        assert_eq!(session.metadata.created_at, created);
922        assert!(session.metadata.last_modified >= initial_modified);
923    }
924}