## Coverage Report

#### Created: 2026-01-25 15:05

next uncovered line (L), next uncovered region (R), next uncovered branch (B)

    /home/noah/src/realizar/src/model_loader.rs

    Line
    Count
    Source
    1
    //! Unified Model Loader
    2
    //!
    3
    //! Per spec §3.2 and §5: Unified loading for APR, GGUF, and SafeTensors formats.
    4
    //!
    5
    //! ## Jidoka (Built-in Quality)
    6
    //!
    7
    //! - Format auto-detection from magic bytes
    8
    //! - CRC32 verification for APR format
    9
    //! - Header validation for SafeTensors
    10
    //! - Graceful error handling with detailed messages
    11
    //!
    12
    //! ## APR Format Support (First-class)
    13
    //!
    14
    //! Per spec §3.1: APR is the primary format for classical ML models from aprender.
    15
    //! Supports all 18 model types:
    16
    //!
    17
    //! | Type | Description |
    18
    //! |------|-------------|
    19
    //! | `LinearRegression` | OLS/Ridge/Lasso |
    20
    //! | `LogisticRegression` | Binary/Multinomial classification |
    21
    //! | `DecisionTree` | CART/ID3 |
    22
    //! | `RandomForest` | Bagging ensemble |
    23
    //! | `GradientBoosting` | Boosting ensemble |
    24
    //! | `KMeans` | Lloyd's clustering |
    25
    //! | `PCA` | Dimensionality reduction |
    26
    //! | `NaiveBayes` | Gaussian NB |
    27
    //! | `KNN` | k-Nearest Neighbors |
    28
    //! | `SVM` | Linear SVM |
    29
    //! | `NgramLM` | N-gram language model |
    30
    //! | `TFIDF` | TF-IDF vectorizer |
    31
    //! | `CountVectorizer` | Count vectorizer |
    32
    //! | `NeuralSequential` | Feed-forward NN |
    33
    //! | `NeuralCustom` | Custom architecture |
    34
    //! | `ContentRecommender` | Content-based rec |
    35
    //! | `MixtureOfExperts` | Sparse/dense MoE |
    36
    //! | `Custom` | User-defined |
    37
    //!
    38
    //! ## GGUF Support (Backwards Compatible)
    39
    //!
    40
    //! Per spec §3.3: GGUF for LLM inference with llama.cpp compatibility.
    41
    //!
    42
    //! ## SafeTensors Support (Backwards Compatible)
    43
    //!
    44
    //! Per spec §3.4: SafeTensors for HuggingFace model weights.
    45
    46
    use std::path::Path;
    47
    48
    use crate::format::{detect_and_verify_format, detect_format, FormatError, ModelFormat};
    49
    50
    /// Model loading errors
    51
    #[derive(Debug, Clone)]
    52
    pub enum LoadError {
    53
        /// Format detection failed
    54
        FormatError(FormatError),
    55
        /// File I/O error
    56
        IoError(String),
    57
        /// Model parsing error
    58
        ParseError(String),
    59
        /// Unsupported model type for serving
    60
        UnsupportedType(String),
    61
        /// CRC32 checksum mismatch (APR)
    62
        IntegrityError(String),
    63
        /// Model type mismatch (requested vs detected)
    64
        TypeMismatch {
    65
            /// Expected model type
    66
            expected: String,
    67
            /// Actual model type in file
    68
            actual: String,
    69
        },
    70
    }
    71
    72
    impl std::fmt::Display for LoadError {
    73
    13
        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
    74
    13
            match self {
    75
    2
                Self::FormatError(e) => write!(f, "Format detection error: {e}"),
    76
    2
                Self::IoError(msg) => write!(f, "I/O error: {msg}"),
    77
    2
                Self::ParseError(msg) => write!(f, "Parse error: {msg}"),
    78
    2
                Self::UnsupportedType(t) => write!(f, "Unsupported model type: {t}"),
    79
    2
                Self::IntegrityError(msg) => write!(f, "Integrity check failed: {msg}"),
    80
    3
                Self::TypeMismatch { expected, actual } => {
    81
    3
                    write!(f, "Model type mismatch: expected {expected}, got {actual}")
    82
                },
    83
            }
    84
    13
        }
    85
    }
    86
    87
    impl std::error::Error for LoadError {}
    88
    89
    impl From<FormatError> for LoadError {
    90
    1
        fn from(e: FormatError) -> Self {
    91
    1
            Self::FormatError(e)
    92
    1
        }
    93
    }
    94
    95
    impl From<std::io::Error> for LoadError {
    96
    0
        fn from(e: std::io::Error) -> Self {
    97
    0
            Self::IoError(e.to_string())
    98
    0
        }
    99
    }
    100
    101
    /// Model metadata extracted during loading
    102
    #[derive(Debug, Clone)]
    103
    pub struct ModelMetadata {
    104
        /// Detected format
    105
        pub format: ModelFormat,
    106
        /// Model type (if detected)
    107
        pub model_type: Option<String>,
    108
        /// Model version
    109
        pub version: Option<String>,
    110
        /// Input dimensions (for validation)
    111
        pub input_dim: Option<usize>,
    112
        /// Output dimensions
    113
        pub output_dim: Option<usize>,
    114
        /// File size in bytes
    115
        pub file_size: u64,
    116
    }
    117
    118
    impl ModelMetadata {
    119
        /// Create new metadata with format only
    120
        #[must_use]
    121
    14
        pub fn new(format: ModelFormat) -> Self {
    122
    14
            Self {
    123
    14
                format,
    124
    14
                model_type: None,
    125
    14
                version: None,
    126
    14
                input_dim: None,
    127
    14
                output_dim: None,
    128
    14
                file_size: 0,
    129
    14
            }
    130
    14
        }
    131
    132
        /// Set model type
    133
        #[must_use]
    134
    4
        pub fn with_model_type(mut self, model_type: impl Into<String>) -> Self {
    135
    4
            self.model_type = Some(model_type.into());
    136
    4
            self
    137
    4
        }
    138
    139
        /// Set version
    140
        #[must_use]
    141
    3
        pub fn with_version(mut self, version: impl Into<String>) -> Self {
    142
    3
            self.version = Some(version.into());
    143
    3
            self
    144
    3
        }
    145
    146
        /// Set input dimensions
    147
        #[must_use]
    148
    3
        pub fn with_input_dim(mut self, dim: usize) -> Self {
    149
    3
            self.input_dim = Some(dim);
    150
    3
            self
    151
    3
        }
    152
    153
        /// Set output dimensions
    154
        #[must_use]
    155
    2
        pub fn with_output_dim(mut self, dim: usize) -> Self {
    156
    2
            self.output_dim = Some(dim);
    157
    2
            self
    158
    2
        }
    159
    160
        /// Set file size
    161
        #[must_use]
    162
    8
        pub fn with_file_size(mut self, size: u64) -> Self {
    163
    8
            self.file_size = size;
    164
    8
            self
    165
    8
        }
    166
    }
    167
    168
    /// Detect model format from file path and contents
    169
    ///
    170
    /// Per spec §3.2: Jidoka - verify both path and magic bytes match.
    171
    ///
    172
    /// # Arguments
    173
    ///
    174
    /// * `path` - Path to model file
    175
    ///
    176
    /// # Returns
    177
    ///
    178
    /// Model metadata with detected format
    179
    ///
    180
    /// # Errors
    181
    ///
    182
    /// Returns error if:
    183
    /// - File cannot be read
    184
    /// - Format cannot be detected
    185
    /// - Extension doesn't match magic bytes
    186
    ///
    187
    /// # Example
    188
    ///
    189
    /// ```rust,ignore
    190
    /// use realizar::model_loader::detect_model;
    191
    /// use std::path::Path;
    192
    ///
    193
    /// let metadata = detect_model(Path::new("model.apr"))?;
    194
    /// assert_eq!(metadata.format, ModelFormat::Apr);
    195
    /// ```
    196
    0
    pub fn detect_model(path: &Path) -> Result<ModelMetadata, LoadError> {
    197
        // Read first 8 bytes for magic detection
    198
    0
        let data = std::fs::read(path)?;
    199
    0
        if data.len() < 8 {
    200
    0
            return Err(LoadError::ParseError(format!(
    201
    0
                "File too small: {} bytes",
    202
    0
                data.len()
    203
    0
            )));
    204
    0
        }
    205
    206
        // Verify format from path and data
    207
    0
        let format = detect_and_verify_format(path, &data[..8])?;
    208
    209
    0
        Ok(ModelMetadata::new(format).with_file_size(data.len() as u64))
    210
    0
    }
    211
    212
    /// Detect model format from bytes only (no path verification)
    213
    ///
    214
    /// Useful for embedded models via `include_bytes!()`.
    215
    ///
    216
    /// # Arguments
    217
    ///
    218
    /// * `data` - Model file bytes
    219
    ///
    220
    /// # Returns
    221
    ///
    222
    /// Model metadata with detected format
    223
    ///
    224
    /// # Errors
    225
    ///
    226
    /// Returns error if:
    227
    /// - Data is too small for format detection (<8 bytes)
    228
    /// - Format cannot be detected from magic bytes
    229
    ///
    230
    /// # Example
    231
    ///
    232
    /// ```rust,ignore
    233
    /// use realizar::model_loader::detect_model_from_bytes;
    234
    ///
    235
    /// const MODEL: &[u8] = include_bytes!("../models/model.apr");
    236
    /// let metadata = detect_model_from_bytes(MODEL)?;
    237
    /// ```
    238
    6
    pub fn detect_model_from_bytes(data: &[u8]) -> Result<ModelMetadata, LoadError> {
    239
    6
        if data.len() < 8 {
    240
    1
            return Err(LoadError::ParseError(format!(
    241
    1
                "Data too small: {} bytes",
    242
    1
                data.len()
    243
    1
            )));
    244
    5
        }
    245
    246
    5
        let format = detect_format(&data[..8])?0;
    247
    248
    5
        Ok(ModelMetadata::new(format).with_file_size(data.len() as u64))
    249
    6
    }
    250
    251
    /// Load APR model type from metadata bytes
    252
    ///
    253
    /// Reads model type from APR header (bytes 4-6 after magic).
    254
    ///
    255
    /// # Arguments
    256
    ///
    257
    /// * `data` - APR file bytes (at least 8 bytes)
    258
    ///
    259
    /// # Returns
    260
    ///
    261
    /// APR model type string (e.g., "LogisticRegression")
    262
    50
    pub fn read_apr_model_type(data: &[u8]) -> Option<String> {
    263
    50
        if data.len() < 8 {
    264
    2
            return None;
    265
    48
        }
    266
    267
        // APR header layout: APRN (4 bytes) + type_id (2 bytes) + version (2 bytes)
    268
        // Per aprender format spec
    269
    48
        let type_id = u16::from_le_bytes([data[4], data[5]]);
    270
    271
        // Map type ID to name (from aprender::format::ModelType)
    272
    48
        let type_name44 = match type_id {
    273
    4
            0x0001 => "LinearRegression",
    274
    5
            0x0002 => "LogisticRegression",
    275
    3
            0x0003 => "DecisionTree",
    276
    3
            0x0004 => "RandomForest",
    277
    2
            0x0005 => "GradientBoosting",
    278
    2
            0x0006 => "KMeans",
    279
    2
            0x0007 => "PCA",
    280
    2
            0x0008 => "NaiveBayes",
    281
    2
            0x0009 => "KNN",
    282
    2
            0x000A => "SVM",
    283
    2
            0x0010 => "NgramLM",
    284
    2
            0x0011 => "TFIDF",
    285
    2
            0x0012 => "CountVectorizer",
    286
    2
            0x0020 => "NeuralSequential",
    287
    2
            0x0021 => "NeuralCustom",
    288
    2
            0x0030 => "ContentRecommender",
    289
    2
            0x0040 => "MixtureOfExperts",
    290
    3
            0x00FF => "Custom",
    291
    4
            _ => return None,
    292
        };
    293
    294
    44
        Some(type_name.to_string())
    295
    50
    }
    296
    297
    /// Validate that loaded model matches expected type
    298
    ///
    299
    /// Per Jidoka: fail fast if type mismatch.
    300
    ///
    301
    /// # Arguments
    302
    ///
    303
    /// * `expected` - Expected model type
    304
    /// * `actual` - Actual model type from file
    305
    ///
    306
    /// # Returns
    307
    ///
    308
    /// Ok if types match, Err otherwise
    309
    ///
    310
    /// # Errors
    311
    ///
    312
    /// Returns `LoadError::TypeMismatch` if expected and actual types differ.
    313
    3
    pub fn validate_model_type(expected: &str, actual: &str) -> Result<(), LoadError> {
    314
    3
        if expected != actual {
    315
    2
            return Err(LoadError::TypeMismatch {
    316
    2
                expected: expected.to_string(),
    317
    2
                actual: actual.to_string(),
    318
    2
            });
    319
    1
        }
    320
    1
        Ok(())
    321
    3
    }
    322
    323
    #[cfg(test)]
    324
    mod tests {
    325
        use super::*;
    326
    327
        // ===== EXTREME TDD: LoadError Tests =====
    328
    329
        #[test]
    330
    1
        fn test_load_error_format_error() {
    331
    1
            let err = LoadError::FormatError(FormatError::UnknownFormat);
    332
    1
            assert!(err.to_string().contains("Format detection error"));
    333
    1
            assert!(err.to_string().contains("Unknown"));
    334
    1
        }
    335
    336
        #[test]
    337
    1
        fn test_load_error_io_error() {
    338
    1
            let err = LoadError::IoError("file not found".to_string());
    339
    1
            assert!(err.to_string().contains("I/O error"));
    340
    1
            assert!(err.to_string().contains("file not found"));
    341
    1
        }
    342
    343
        #[test]
    344
    1
        fn test_load_error_parse_error() {
    345
    1
            let err = LoadError::ParseError("invalid header".to_string());
    346
    1
            assert!(err.to_string().contains("Parse error"));
    347
    1
            assert!(err.to_string().contains("invalid header"));
    348
    1
        }
    349
    350
        #[test]
    351
    1
        fn test_load_error_unsupported_type() {
    352
    1
            let err = LoadError::UnsupportedType("UnknownModel".to_string());
    353
    1
            assert!(err.to_string().contains("Unsupported model type"));
    354
    1
            assert!(err.to_string().contains("UnknownModel"));
    355
    1
        }
    356
    357
        #[test]
    358
    1
        fn test_load_error_integrity_error() {
    359
    1
            let err = LoadError::IntegrityError("CRC32 mismatch".to_string());
    360
    1
            assert!(err.to_string().contains("Integrity check failed"));
    361
    1
            assert!(err.to_string().contains("CRC32"));
    362
    1
        }
    363
    364
        #[test]
    365
    1
        fn test_load_error_type_mismatch() {
    366
    1
            let err = LoadError::TypeMismatch {
    367
    1
                expected: "LogisticRegression".to_string(),
    368
    1
                actual: "DecisionTree".to_string(),
    369
    1
            };
    370
    1
            assert!(err.to_string().contains("type mismatch"));
    371
    1
            assert!(err.to_string().contains("LogisticRegression"));
    372
    1
            assert!(err.to_string().contains("DecisionTree"));
    373
    1
        }
    374
    375
        #[test]
    376
    1
        fn test_load_error_from_format_error() {
    377
    1
            let format_err = FormatError::TooShort { len: 3 };
    378
    1
            let load_err: LoadError = format_err.into();
    379
    1
            assert!(matches!0(load_err, LoadError::FormatError(_)));
    380
    1
        }
    381
    382
        // ===== EXTREME TDD: ModelMetadata Tests =====
    383
    384
        #[test]
    385
    1
        fn test_model_metadata_new() {
    386
    1
            let meta = ModelMetadata::new(ModelFormat::Apr);
    387
    1
            assert_eq!(meta.format, ModelFormat::Apr);
    388
    1
            assert!(meta.model_type.is_none());
    389
    1
            assert!(meta.version.is_none());
    390
    1
            assert!(meta.input_dim.is_none());
    391
    1
            assert!(meta.output_dim.is_none());
    392
    1
            assert_eq!(meta.file_size, 0);
    393
    1
        }
    394
    395
        #[test]
    396
    1
        fn test_model_metadata_with_model_type() {
    397
    1
            let meta = ModelMetadata::new(ModelFormat::Apr).with_model_type("LogisticRegression");
    398
    1
            assert_eq!(meta.model_type, Some("LogisticRegression".to_string()));
    399
    1
        }
    400
    401
        #[test]
    402
    1
        fn test_model_metadata_with_version() {
    403
    1
            let meta = ModelMetadata::new(ModelFormat::Gguf).with_version("v1.0.0");
    404
    1
            assert_eq!(meta.version, Some("v1.0.0".to_string()));
    405
    1
        }
    406
    407
        #[test]
    408
    1
        fn test_model_metadata_with_input_dim() {
    409
    1
            let meta = ModelMetadata::new(ModelFormat::SafeTensors).with_input_dim(784);
    410
    1
            assert_eq!(meta.input_dim, Some(784));
    411
    1
        }
    412
    413
        #[test]
    414
    1
        fn test_model_metadata_with_output_dim() {
    415
    1
            let meta = ModelMetadata::new(ModelFormat::Apr).with_output_dim(10);
    416
    1
            assert_eq!(meta.output_dim, Some(10));
    417
    1
        }
    418
    419
        #[test]
    420
    1
        fn test_model_metadata_with_file_size() {
    421
    1
            let meta = ModelMetadata::new(ModelFormat::Gguf).with_file_size(1_000_000);
    422
    1
            assert_eq!(meta.file_size, 1_000_000);
    423
    1
        }
    424
    425
        #[test]
    426
    1
        fn test_model_metadata_chained_builders() {
    427
    1
            let meta = ModelMetadata::new(ModelFormat::Apr)
    428
    1
                .with_model_type("RandomForest")
    429
    1
                .with_version("v2.1")
    430
    1
                .with_input_dim(128)
    431
    1
                .with_output_dim(3)
    432
    1
                .with_file_size(50_000);
    433
    434
    1
            assert_eq!(meta.format, ModelFormat::Apr);
    435
    1
            assert_eq!(meta.model_type, Some("RandomForest".to_string()));
    436
    1
            assert_eq!(meta.version, Some("v2.1".to_string()));
    437
    1
            assert_eq!(meta.input_dim, Some(128));
    438
    1
            assert_eq!(meta.output_dim, Some(3));
    439
    1
            assert_eq!(meta.file_size, 50_000);
    440
    1
        }
    441
    442
        // ===== EXTREME TDD: detect_model_from_bytes Tests =====
    443
    444
        #[test]
    445
    1
        fn test_detect_model_from_bytes_apr() {
    446
    1
            let mut data = b"APR\0".to_vec();
    447
    1
            data.extend_from_slice(&[0x02, 0x00, 0x01, 0x00]); // LogisticRegression type
    448
    1
            data.extend_from_slice(&[0u8; 100]); // Padding
    449
    450
    1
            let meta = detect_model_from_bytes(&data).expect("Should detect APR");
    451
    1
            assert_eq!(meta.format, ModelFormat::Apr);
    452
    1
            assert_eq!(meta.file_size, 108);
    453
    1
        }
    454
    455
        #[test]
    456
    1
        fn test_detect_model_from_bytes_gguf() {
    457
    1
            let mut data = b"GGUF".to_vec();
    458
    1
            data.extend_from_slice(&[0u8; 100]); // Padding
    459
    460
    1
            let meta = detect_model_from_bytes(&data).expect("Should detect GGUF");
    461
    1
            assert_eq!(meta.format, ModelFormat::Gguf);
    462
    1
        }
    463
    464
        #[test]
    465
    1
        fn test_detect_model_from_bytes_safetensors() {
    466
    1
            let header_size: u64 = 100;
    467
    1
            let mut data = header_size.to_le_bytes().to_vec();
    468
    1
            data.extend_from_slice(&[0u8; 200]);
    469
    470
    1
            let meta = detect_model_from_bytes(&data).expect("Should detect SafeTensors");
    471
    1
            assert_eq!(meta.format, ModelFormat::SafeTensors);
    472
    1
        }
    473
    474
        #[test]
    475
    1
        fn test_detect_model_from_bytes_too_small() {
    476
    1
            let data = b"APR";
    477
    1
            let result = detect_model_from_bytes(data);
    478
    1
            assert!(result.is_err());
    479
    1
            assert!(matches!0(result.unwrap_err(), LoadError::ParseError(_)));
    480
    1
        }
    481
    482
        // ===== EXTREME TDD: read_apr_model_type Tests =====
    483
    484
        #[test]
    485
    1
        fn test_read_apr_model_type_linear_regression() {
    486
    1
            let mut data = b"APR\0".to_vec();
    487
    1
            data.extend_from_slice(&0x0001u16.to_le_bytes());
    488
    1
            data.extend_from_slice(&[0, 0]);
    489
    490
    1
            assert_eq!(
    491
    1
                read_apr_model_type(&data),
    492
    1
                Some("LinearRegression".to_string())
    493
            );
    494
    1
        }
    495
    496
        #[test]
    497
    1
        fn test_read_apr_model_type_logistic_regression() {
    498
    1
            let mut data = b"APR\0".to_vec();
    499
    1
            data.extend_from_slice(&0x0002u16.to_le_bytes());
    500
    1
            data.extend_from_slice(&[0, 0]);
    501
    502
    1
            assert_eq!(
    503
    1
                read_apr_model_type(&data),
    504
    1
                Some("LogisticRegression".to_string())
    505
            );
    506
    1
        }
    507
    508
        #[test]
    509
    1
        fn test_read_apr_model_type_decision_tree() {
    510
    1
            let mut data = b"APR\0".to_vec();
    511
    1
            data.extend_from_slice(&0x0003u16.to_le_bytes());
    512
    1
            data.extend_from_slice(&[0, 0]);
    513
    514
    1
            assert_eq!(read_apr_model_type(&data), Some("DecisionTree".to_string()));
    515
    1
        }
    516
    517
        #[test]
    518
    1
        fn test_read_apr_model_type_random_forest() {
    519
    1
            let mut data = b"APR\0".to_vec();
    520
    1
            data.extend_from_slice(&0x0004u16.to_le_bytes());
    521
    1
            data.extend_from_slice(&[0, 0]);
    522
    523
    1
            assert_eq!(read_apr_model_type(&data), Some("RandomForest".to_string()));
    524
    1
        }
    525
    526
        #[test]
    527
    1
        fn test_read_apr_model_type_gradient_boosting() {
    528
    1
            let mut data = b"APR\0".to_vec();
    529
    1
            data.extend_from_slice(&0x0005u16.to_le_bytes());
    530
    1
            data.extend_from_slice(&[0, 0]);
    531
    532
    1
            assert_eq!(
    533
    1
                read_apr_model_type(&data),
    534
    1
                Some("GradientBoosting".to_string())
    535
            );
    536
    1
        }
    537
    538
        #[test]
    539
    1
        fn test_read_apr_model_type_kmeans() {
    540
    1
            let mut data = b"APR\0".to_vec();
    541
    1
            data.extend_from_slice(&0x0006u16.to_le_bytes());
    542
    1
            data.extend_from_slice(&[0, 0]);
    543
    544
    1
            assert_eq!(read_apr_model_type(&data), Some("KMeans".to_string()));
    545
    1
        }
    546
    547
        #[test]
    548
    1
        fn test_read_apr_model_type_pca() {
    549
    1
            let mut data = b"APR\0".to_vec();
    550
    1
            data.extend_from_slice(&0x0007u16.to_le_bytes());
    551
    1
            data.extend_from_slice(&[0, 0]);
    552
    553
    1
            assert_eq!(read_apr_model_type(&data), Some("PCA".to_string()));
    554
    1
        }
    555
    556
        #[test]
    557
    1
        fn test_read_apr_model_type_naive_bayes() {
    558
    1
            let mut data = b"APR\0".to_vec();
    559
    1
            data.extend_from_slice(&0x0008u16.to_le_bytes());
    560
    1
            data.extend_from_slice(&[0, 0]);
    561
    562
    1
            assert_eq!(read_apr_model_type(&data), Some("NaiveBayes".to_string()));
    563
    1
        }
    564
    565
        #[test]
    566
    1
        fn test_read_apr_model_type_knn() {
    567
    1
            let mut data = b"APR\0".to_vec();
    568
    1
            data.extend_from_slice(&0x0009u16.to_le_bytes());
    569
    1
            data.extend_from_slice(&[0, 0]);
    570
    571
    1
            assert_eq!(read_apr_model_type(&data), Some("KNN".to_string()));
    572
    1
        }
    573
    574
        #[test]
    575
    1
        fn test_read_apr_model_type_svm() {
    576
    1
            let mut data = b"APR\0".to_vec();
    577
    1
            data.extend_from_slice(&0x000Au16.to_le_bytes());
    578
    1
            data.extend_from_slice(&[0, 0]);
    579
    580
    1
            assert_eq!(read_apr_model_type(&data), Some("SVM".to_string()));
    581
    1
        }
    582
    583
        #[test]
    584
    1
        fn test_read_apr_model_type_ngram_lm() {
    585
    1
            let mut data = b"APR\0".to_vec();
    586
    1
            data.extend_from_slice(&0x0010u16.to_le_bytes());
    587
    1
            data.extend_from_slice(&[0, 0]);
    588
    589
    1
            assert_eq!(read_apr_model_type(&data), Some("NgramLM".to_string()));
    590
    1
        }
    591
    592
        #[test]
    593
    1
        fn test_read_apr_model_type_tfidf() {
    594
    1
            let mut data = b"APR\0".to_vec();
    595
    1
            data.extend_from_slice(&0x0011u16.to_le_bytes());
    596
    1
            data.extend_from_slice(&[0, 0]);
    597
    598
    1
            assert_eq!(read_apr_model_type(&data), Some("TFIDF".to_string()));
    599
    1
        }
    600
    601
        #[test]
    602
    1
        fn test_read_apr_model_type_count_vectorizer() {
    603
    1
            let mut data = b"APR\0".to_vec();
    604
    1
            data.extend_from_slice(&0x0012u16.to_le_bytes());
    605
    1
            data.extend_from_slice(&[0, 0]);
    606
    607
    1
            assert_eq!(
    608
    1
                read_apr_model_type(&data),
    609
    1
                Some("CountVectorizer".to_string())
    610
            );
    611
    1
        }
    612
    613
        #[test]
    614
    1
        fn test_read_apr_model_type_neural_sequential() {
    615
    1
            let mut data = b"APR\0".to_vec();
    616
    1
            data.extend_from_slice(&0x0020u16.to_le_bytes());
    617
    1
            data.extend_from_slice(&[0, 0]);
    618
    619
    1
            assert_eq!(
    620
    1
                read_apr_model_type(&data),
    621
    1
                Some("NeuralSequential".to_string())
    622
            );
    623
    1
        }
    624
    625
        #[test]
    626
    1
        fn test_read_apr_model_type_neural_custom() {
    627
    1
            let mut data = b"APR\0".to_vec();
    628
    1
            data.extend_from_slice(&0x0021u16.to_le_bytes());
    629
    1
            data.extend_from_slice(&[0, 0]);
    630
    631
    1
            assert_eq!(read_apr_model_type(&data), Some("NeuralCustom".to_string()));
    632
    1
        }
    633
    634
        #[test]
    635
    1
        fn test_read_apr_model_type_content_recommender() {
    636
    1
            let mut data = b"APR\0".to_vec();
    637
    1
            data.extend_from_slice(&0x0030u16.to_le_bytes());
    638
    1
            data.extend_from_slice(&[0, 0]);
    639
    640
    1
            assert_eq!(
    641
    1
                read_apr_model_type(&data),
    642
    1
                Some("ContentRecommender".to_string())
    643
            );
    644
    1
        }
    645
    646
        #[test]
    647
    1
        fn test_read_apr_model_type_mixture_of_experts() {
    648
    1
            let mut data = b"APR\0".to_vec();
    649
    1
            data.extend_from_slice(&0x0040u16.to_le_bytes());
    650
    1
            data.extend_from_slice(&[0, 0]);
    651
    652
    1
            assert_eq!(
    653
    1
                read_apr_model_type(&data),
    654
    1
                Some("MixtureOfExperts".to_string())
    655
            );
    656
    1
        }
    657
    658
        #[test]
    659
    1
        fn test_read_apr_model_type_custom() {
    660
    1
            let mut data = b"APR\0".to_vec();
    661
    1
            data.extend_from_slice(&0x00FFu16.to_le_bytes());
    662
    1
            data.extend_from_slice(&[0, 0]);
    663
    664
    1
            assert_eq!(read_apr_model_type(&data), Some("Custom".to_string()));
    665
    1
        }
    666
    667
        #[test]
    668
    1
        fn test_read_apr_model_type_unknown() {
    669
    1
            let mut data = b"APR\0".to_vec();
    670
    1
            data.extend_from_slice(&0xFFFFu16.to_le_bytes()); // Unknown type
    671
    1
            data.extend_from_slice(&[0, 0]);
    672
    673
    1
            assert_eq!(read_apr_model_type(&data), None);
    674
    1
        }
    675
    676
        #[test]
    677
    1
        fn test_read_apr_model_type_too_short() {
    678
    1
            let data = b"APR\0"; // Only 4 bytes
    679
    1
            assert_eq!(read_apr_model_type(data), None);
    680
    1
        }
    681
    682
        // ===== EXTREME TDD: validate_model_type Tests =====
    683
    684
        #[test]
    685
    1
        fn test_validate_model_type_match() {
    686
    1
            let result = validate_model_type("LogisticRegression", "LogisticRegression");
    687
    1
            assert!(result.is_ok());
    688
    1
        }
    689
    690
        #[test]
    691
    1
        fn test_validate_model_type_mismatch() {
    692
    1
            let result = validate_model_type("LogisticRegression", "DecisionTree");
    693
    1
            assert!(result.is_err());
    694
    695
    1
            if let Err(LoadError::TypeMismatch { expected, actual }) = result {
    696
    1
                assert_eq!(expected, "LogisticRegression");
    697
    1
                assert_eq!(actual, "DecisionTree");
    698
            } else {
    699
    0
                panic!("Expected TypeMismatch error");
    700
            }
    701
    1
        }
    702
    703
        #[test]
    704
    1
        fn test_validate_model_type_case_sensitive() {
    705
            // Type names are case-sensitive
    706
    1
            let result = validate_model_type("logisticregression", "LogisticRegression");
    707
    1
            assert!(result.is_err());
    708
    1
        }
    709
    710
        // ===== EXTREME TDD: Integration Tests =====
    711
    712
        #[test]
    713
    1
        fn test_detect_and_extract_apr_type() {
    714
            // Simulate APR file with LogisticRegression type
    715
    1
            let mut data = b"APR\0".to_vec();
    716
    1
            data.extend_from_slice(&0x0002u16.to_le_bytes()); // LogisticRegression
    717
    1
            data.extend_from_slice(&[0, 0]); // version placeholder
    718
    1
            data.extend_from_slice(&[0u8; 100]); // Padding
    719
    720
    1
            let meta = detect_model_from_bytes(&data).expect("Detection should succeed");
    721
    1
            assert_eq!(meta.format, ModelFormat::Apr);
    722
    723
    1
            let model_type = read_apr_model_type(&data).expect("Should extract model type");
    724
    1
            assert_eq!(model_type, "LogisticRegression");
    725
    1
        }
    726
    727
        #[test]
    728
    1
        fn test_full_metadata_extraction() {
    729
    1
            let mut data = b"APR\0".to_vec();
    730
    1
            data.extend_from_slice(&0x0004u16.to_le_bytes()); // RandomForest
    731
    1
            data.extend_from_slice(&[0, 0]);
    732
    1
            data.extend_from_slice(&[0u8; 500]);
    733
    734
    1
            let meta = detect_model_from_bytes(&data)
    735
    1
                .expect("Detection should succeed")
    736
    1
                .with_model_type(read_apr_model_type(&data).unwrap_or_default())
    737
    1
                .with_version("v1.0")
    738
    1
                .with_input_dim(128);
    739
    740
    1
            assert_eq!(meta.format, ModelFormat::Apr);
    741
    1
            assert_eq!(meta.model_type, Some("RandomForest".to_string()));
    742
    1
            assert_eq!(meta.version, Some("v1.0".to_string()));
    743
    1
            assert_eq!(meta.input_dim, Some(128));
    744
    1
            assert_eq!(meta.file_size, 508);
    745
    1
        }
    746
    747
        // ===== EXTREME TDD: Debug/Error Trait Tests =====
    748
    749
        #[test]
    750
    1
        fn test_load_error_debug() {
    751
    1
            let err = LoadError::IoError("test".to_string());
    752
    1
            let debug_str = format!("{err:?}");
    753
    1
            assert!(debug_str.contains("IoError"));
    754
    1
        }
    755
    756
        #[test]
    757
    1
        fn test_model_metadata_debug() {
    758
    1
            let meta = ModelMetadata::new(ModelFormat::Apr);
    759
    1
            let debug_str = format!("{meta:?}");
    760
    1
            assert!(debug_str.contains("Apr"));
    761
    1
        }
    762
    763
        #[test]
    764
    1
        fn test_model_metadata_clone() {
    765
    1
            let meta = ModelMetadata::new(ModelFormat::Gguf)
    766
    1
                .with_model_type("LLM")
    767
    1
                .with_file_size(1000);
    768
    1
            let cloned = meta.clone();
    769
    770
    1
            assert_eq!(cloned.format, ModelFormat::Gguf);
    771
    1
            assert_eq!(cloned.model_type, Some("LLM".to_string()));
    772
    1
            assert_eq!(cloned.file_size, 1000);
    773
    1
        }
    774
    775
        #[test]
    776
    1
        fn test_load_error_clone() {
    777
    1
            let err = LoadError::ParseError("test".to_string());
    778
    1
            let cloned = err.clone();
    779
    1
            assert!(matches!0(cloned, LoadError::ParseError(_)));
    780
    1
        }
    781
    }