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
}