Skip to main content

smith_protocol/
result_schema.rs

1//! Result Schema v1 - Locked Contract for Smith Platform
2//!
3//! This module defines the locked Result Schema v1 with strict validation
4//! to prevent breaking changes while allowing controlled extensibility.
5
6use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8use thiserror::Error;
9
10/// Result Schema Version - MUST match across all Smith components
11pub const RESULT_SCHEMA_VERSION: u32 = 1;
12
13/// Locked Result Schema v1 - These fields are immutable
14///
15/// Any changes to these fields require a MAJOR version bump.
16/// New fields can only be added to `x_meta` for extensibility.
17#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
18pub struct ResultSchemaV1 {
19    /// Execution success indicator - LOCKED FIELD
20    pub ok: bool,
21
22    /// Human-readable status description - LOCKED FIELD  
23    pub status: String,
24
25    /// Execution latency in milliseconds - LOCKED FIELD
26    pub latency_ms: u64,
27
28    /// Number of bytes processed/returned - LOCKED FIELD
29    pub bytes: u64,
30
31    /// Capability bundle digest used for execution - LOCKED FIELD
32    pub capability_digest: String,
33
34    /// Git commit hash of executor version - LOCKED FIELD
35    pub commit: String,
36
37    /// Execution layer (atom/macro/playbook) - LOCKED FIELD
38    pub layer: String,
39
40    /// Capability or component name - LOCKED FIELD
41    pub name: String,
42
43    /// Execution mode (strict/explore/shadow) - LOCKED FIELD
44    pub mode: String,
45
46    /// Experiment ID for A/B testing - LOCKED FIELD
47    pub exp_id: String,
48
49    /// Idempotency key for duplicate prevention - LOCKED FIELD
50    pub idem_key: String,
51
52    /// Extension metadata - ONLY place for new fields - LOCKED FIELD
53    /// New fields MUST be added here, not as top-level fields
54    #[serde(default)]
55    pub x_meta: HashMap<String, serde_json::Value>,
56}
57
58impl ResultSchemaV1 {
59    /// Create a new result with required fields
60    #[allow(clippy::too_many_arguments)]
61    pub fn new(
62        ok: bool,
63        status: String,
64        latency_ms: u64,
65        bytes: u64,
66        capability_digest: String,
67        commit: String,
68        layer: String,
69        name: String,
70        mode: String,
71        exp_id: String,
72        idem_key: String,
73    ) -> Self {
74        Self {
75            ok,
76            status,
77            latency_ms,
78            bytes,
79            capability_digest,
80            commit,
81            layer,
82            name,
83            mode,
84            exp_id,
85            idem_key,
86            x_meta: HashMap::new(),
87        }
88    }
89
90    /// Add extension metadata (only way to extend schema)
91    pub fn with_meta(mut self, key: String, value: serde_json::Value) -> Self {
92        self.x_meta.insert(key, value);
93        self
94    }
95
96    /// Get extension metadata
97    pub fn get_meta(&self, key: &str) -> Option<&serde_json::Value> {
98        self.x_meta.get(key)
99    }
100
101    /// Validate schema compliance
102    pub fn validate(&self) -> Result<(), ResultSchemaError> {
103        // Validate required string fields are not empty
104        if self.status.is_empty() {
105            return Err(ResultSchemaError::EmptyRequiredField("status".to_string()));
106        }
107        if self.capability_digest.is_empty() {
108            return Err(ResultSchemaError::EmptyRequiredField(
109                "capability_digest".to_string(),
110            ));
111        }
112        if self.commit.is_empty() {
113            return Err(ResultSchemaError::EmptyRequiredField("commit".to_string()));
114        }
115        if self.layer.is_empty() {
116            return Err(ResultSchemaError::EmptyRequiredField("layer".to_string()));
117        }
118        if self.name.is_empty() {
119            return Err(ResultSchemaError::EmptyRequiredField("name".to_string()));
120        }
121        if self.mode.is_empty() {
122            return Err(ResultSchemaError::EmptyRequiredField("mode".to_string()));
123        }
124        if self.exp_id.is_empty() {
125            return Err(ResultSchemaError::EmptyRequiredField("exp_id".to_string()));
126        }
127        if self.idem_key.is_empty() {
128            return Err(ResultSchemaError::EmptyRequiredField(
129                "idem_key".to_string(),
130            ));
131        }
132
133        // Validate field constraints
134        if !matches!(self.layer.as_str(), "atom" | "macro" | "playbook") {
135            return Err(ResultSchemaError::InvalidFieldValue {
136                field: "layer".to_string(),
137                value: self.layer.clone(),
138                allowed: vec![
139                    "atom".to_string(),
140                    "macro".to_string(),
141                    "playbook".to_string(),
142                ],
143            });
144        }
145
146        if !matches!(self.mode.as_str(), "strict" | "explore" | "shadow") {
147            return Err(ResultSchemaError::InvalidFieldValue {
148                field: "mode".to_string(),
149                value: self.mode.clone(),
150                allowed: vec![
151                    "strict".to_string(),
152                    "explore".to_string(),
153                    "shadow".to_string(),
154                ],
155            });
156        }
157
158        // Validate capability digest format (SHA256 hex)
159        if self.capability_digest.len() != 64
160            || !self
161                .capability_digest
162                .chars()
163                .all(|c| c.is_ascii_hexdigit())
164        {
165            return Err(ResultSchemaError::InvalidFieldFormat {
166                field: "capability_digest".to_string(),
167                expected: "64-character SHA256 hex string".to_string(),
168                actual: self.capability_digest.clone(),
169            });
170        }
171
172        // Validate commit hash format (7-40 char hex)
173        if self.commit.len() < 7
174            || self.commit.len() > 40
175            || !self.commit.chars().all(|c| c.is_ascii_hexdigit())
176        {
177            return Err(ResultSchemaError::InvalidFieldFormat {
178                field: "commit".to_string(),
179                expected: "7-40 character git commit hex string".to_string(),
180                actual: self.commit.clone(),
181            });
182        }
183
184        // Validate idempotency key format
185        if !self.idem_key.starts_with("idem_") || self.idem_key.len() != 21 {
186            return Err(ResultSchemaError::InvalidFieldFormat {
187                field: "idem_key".to_string(),
188                expected: "idem_<16-hex-chars> format".to_string(),
189                actual: self.idem_key.clone(),
190            });
191        }
192
193        Ok(())
194    }
195}
196
197/// Result Schema validation errors
198#[derive(Debug, Error)]
199pub enum ResultSchemaError {
200    #[error("Empty required field: {0}")]
201    EmptyRequiredField(String),
202
203    #[error("Invalid value for field {field}: '{value}', allowed: {allowed:?}")]
204    InvalidFieldValue {
205        field: String,
206        value: String,
207        allowed: Vec<String>,
208    },
209
210    #[error("Invalid format for field {field}: expected {expected}, got '{actual}'")]
211    InvalidFieldFormat {
212        field: String,
213        expected: String,
214        actual: String,
215    },
216
217    #[error("Unknown field detected: {field}. Only x_meta extensions are allowed.")]
218    UnknownField { field: String },
219
220    #[error("Schema version mismatch: expected v{expected}, got v{actual}")]
221    VersionMismatch { expected: u32, actual: u32 },
222
223    #[error("Deserialization failed: {0}")]
224    DeserializationFailed(String),
225}
226
227/// Strict result validator that rejects unknown fields
228pub struct ResultSchemaValidator;
229
230impl ResultSchemaValidator {
231    /// Validate JSON against locked schema v1
232    ///
233    /// This validator REJECTS unknown fields (except in x_meta) to enforce
234    /// backward compatibility and prevent breaking changes.
235    pub fn validate_json(json_str: &str) -> Result<ResultSchemaV1, ResultSchemaError> {
236        // Parse JSON to check for unknown fields
237        let json_value: serde_json::Value = serde_json::from_str(json_str)
238            .map_err(|e| ResultSchemaError::DeserializationFailed(e.to_string()))?;
239
240        if let Some(obj) = json_value.as_object() {
241            // Check for unknown top-level fields
242            let allowed_fields = &[
243                "ok",
244                "status",
245                "latency_ms",
246                "bytes",
247                "capability_digest",
248                "commit",
249                "layer",
250                "name",
251                "mode",
252                "exp_id",
253                "idem_key",
254                "x_meta",
255            ];
256
257            for field_name in obj.keys() {
258                if !allowed_fields.contains(&field_name.as_str()) {
259                    return Err(ResultSchemaError::UnknownField {
260                        field: field_name.clone(),
261                    });
262                }
263            }
264        }
265
266        // Deserialize to struct
267        let result: ResultSchemaV1 = serde_json::from_str(json_str)
268            .map_err(|e| ResultSchemaError::DeserializationFailed(e.to_string()))?;
269
270        // Validate field constraints
271        result.validate()?;
272
273        Ok(result)
274    }
275
276    /// Validate a pre-parsed ResultSchemaV1
277    pub fn validate_struct(result: &ResultSchemaV1) -> Result<(), ResultSchemaError> {
278        result.validate()
279    }
280
281    /// Check backward compatibility with previous result
282    pub fn check_backward_compatibility(
283        _old_result: &ResultSchemaV1,
284        _new_result: &ResultSchemaV1,
285    ) -> Result<(), ResultSchemaError> {
286        // All locked fields must have same types and constraints
287        // This is enforced by the struct definition and validation
288
289        // x_meta can change freely (extensibility point)
290        Ok(())
291    }
292
293    /// Generate schema hash for ABI stability checks
294    pub fn schema_hash() -> String {
295        use sha2::{Digest, Sha256};
296
297        // Create deterministic representation of schema fields
298        let schema_repr = format!(
299            "RESULT_SCHEMA_V{}_FIELDS:ok:bool,status:string,latency_ms:u64,bytes:u64,capability_digest:string,commit:string,layer:string,name:string,mode:string,exp_id:string,idem_key:string,x_meta:map",
300            RESULT_SCHEMA_VERSION
301        );
302
303        let mut hasher = Sha256::new();
304        hasher.update(schema_repr.as_bytes());
305        format!("{:x}", hasher.finalize())
306    }
307}
308
309/// Helper functions for creating results with common patterns
310pub mod builders {
311    use super::*;
312
313    /// Create a successful result
314    #[allow(clippy::too_many_arguments)]
315    pub fn success(
316        latency_ms: u64,
317        bytes: u64,
318        capability_digest: String,
319        commit: String,
320        name: String,
321        mode: String,
322        exp_id: String,
323        idem_key: String,
324    ) -> ResultSchemaV1 {
325        ResultSchemaV1::new(
326            true,
327            "success".to_string(),
328            latency_ms,
329            bytes,
330            capability_digest,
331            commit,
332            "atom".to_string(), // default layer
333            name,
334            mode,
335            exp_id,
336            idem_key,
337        )
338    }
339
340    /// Create an error result
341    #[allow(clippy::too_many_arguments)]
342    pub fn error(
343        error_message: String,
344        latency_ms: u64,
345        capability_digest: String,
346        commit: String,
347        name: String,
348        mode: String,
349        exp_id: String,
350        idem_key: String,
351    ) -> ResultSchemaV1 {
352        ResultSchemaV1::new(
353            false,
354            format!("error: {}", error_message),
355            latency_ms,
356            0, // no bytes processed on error
357            capability_digest,
358            commit,
359            "atom".to_string(),
360            name,
361            mode,
362            exp_id,
363            idem_key,
364        )
365    }
366
367    /// Create a timeout result
368    pub fn timeout(
369        capability_digest: String,
370        commit: String,
371        name: String,
372        mode: String,
373        exp_id: String,
374        idem_key: String,
375    ) -> ResultSchemaV1 {
376        ResultSchemaV1::new(
377            false,
378            "timeout".to_string(),
379            0, // unknown latency
380            0, // no bytes processed
381            capability_digest,
382            commit,
383            "atom".to_string(),
384            name,
385            mode,
386            exp_id,
387            idem_key,
388        )
389    }
390}
391
392#[cfg(test)]
393mod tests {
394    use super::*;
395    use serde_json::json;
396
397    fn create_valid_result() -> ResultSchemaV1 {
398        ResultSchemaV1::new(
399            true,
400            "success".to_string(),
401            150,
402            1024,
403            "a".repeat(64),        // valid SHA256
404            "abc123f".to_string(), // valid commit
405            "atom".to_string(),
406            "fs.read.v1".to_string(),
407            "strict".to_string(),
408            "exp_123".to_string(),
409            "idem_1234567890abcdef".to_string(),
410        )
411    }
412
413    #[test]
414    fn test_valid_result_creation() {
415        let result = create_valid_result();
416        assert!(result.validate().is_ok());
417        assert!(result.ok);
418        assert_eq!(result.status, "success");
419        assert_eq!(result.latency_ms, 150);
420        assert_eq!(result.bytes, 1024);
421    }
422
423    #[test]
424    fn test_result_with_metadata() {
425        let result = create_valid_result()
426            .with_meta("custom_field".to_string(), json!("custom_value"))
427            .with_meta("debug_info".to_string(), json!({"details": "test"}));
428
429        assert!(result.validate().is_ok());
430        assert_eq!(
431            result.get_meta("custom_field"),
432            Some(&json!("custom_value"))
433        );
434        assert_eq!(
435            result.get_meta("debug_info"),
436            Some(&json!({"details": "test"}))
437        );
438        assert_eq!(result.get_meta("nonexistent"), None);
439    }
440
441    #[test]
442    fn test_validation_empty_fields() {
443        let mut result = create_valid_result();
444        result.status = "".to_string();
445
446        let error = result.validate().unwrap_err();
447        assert!(matches!(error, ResultSchemaError::EmptyRequiredField(_)));
448    }
449
450    #[test]
451    fn test_validation_invalid_layer() {
452        let mut result = create_valid_result();
453        result.layer = "invalid_layer".to_string();
454
455        let error = result.validate().unwrap_err();
456        assert!(matches!(error, ResultSchemaError::InvalidFieldValue { .. }));
457    }
458
459    #[test]
460    fn test_validation_invalid_mode() {
461        let mut result = create_valid_result();
462        result.mode = "invalid_mode".to_string();
463
464        let error = result.validate().unwrap_err();
465        assert!(matches!(error, ResultSchemaError::InvalidFieldValue { .. }));
466    }
467
468    #[test]
469    fn test_validation_invalid_capability_digest() {
470        let mut result = create_valid_result();
471        result.capability_digest = "invalid_digest".to_string();
472
473        let error = result.validate().unwrap_err();
474        assert!(matches!(
475            error,
476            ResultSchemaError::InvalidFieldFormat { .. }
477        ));
478    }
479
480    #[test]
481    fn test_validation_invalid_commit() {
482        let mut result = create_valid_result();
483        result.commit = "x".to_string(); // too short
484
485        let error = result.validate().unwrap_err();
486        assert!(matches!(
487            error,
488            ResultSchemaError::InvalidFieldFormat { .. }
489        ));
490    }
491
492    #[test]
493    fn test_validation_invalid_idem_key() {
494        let mut result = create_valid_result();
495        result.idem_key = "invalid_key".to_string();
496
497        let error = result.validate().unwrap_err();
498        assert!(matches!(
499            error,
500            ResultSchemaError::InvalidFieldFormat { .. }
501        ));
502    }
503
504    #[test]
505    fn test_json_validation_success() {
506        let valid_json = json!({
507            "ok": true,
508            "status": "success",
509            "latency_ms": 150,
510            "bytes": 1024,
511            "capability_digest": "a".repeat(64),
512            "commit": "abc123f",
513            "layer": "atom",
514            "name": "fs.read.v1",
515            "mode": "strict",
516            "exp_id": "exp_123",
517            "idem_key": "idem_1234567890abcdef",
518            "x_meta": {
519                "custom": "value"
520            }
521        })
522        .to_string();
523
524        let result = ResultSchemaValidator::validate_json(&valid_json).unwrap();
525        assert!(result.ok);
526        assert_eq!(result.get_meta("custom"), Some(&json!("value")));
527    }
528
529    #[test]
530    fn test_json_validation_unknown_field() {
531        let invalid_json = json!({
532            "ok": true,
533            "status": "success",
534            "latency_ms": 150,
535            "bytes": 1024,
536            "capability_digest": "a".repeat(64),
537            "commit": "abc123f",
538            "layer": "atom",
539            "name": "fs.read.v1",
540            "mode": "strict",
541            "exp_id": "exp_123",
542            "idem_key": "idem_1234567890abcdef",
543            "unknown_field": "not allowed" // This should cause validation to fail
544        })
545        .to_string();
546
547        let error = ResultSchemaValidator::validate_json(&invalid_json).unwrap_err();
548        assert!(matches!(error, ResultSchemaError::UnknownField { .. }));
549    }
550
551    #[test]
552    fn test_serialization_roundtrip() {
553        let original = create_valid_result().with_meta("test".to_string(), json!("metadata"));
554
555        let json = serde_json::to_string(&original).unwrap();
556        let deserialized = ResultSchemaValidator::validate_json(&json).unwrap();
557
558        assert_eq!(original, deserialized);
559    }
560
561    #[test]
562    fn test_builder_functions() {
563        let success = builders::success(
564            100,
565            512,
566            "a".repeat(64),
567            "abcdef123".to_string(), // Valid 9-char hex commit hash
568            "test.capability".to_string(),
569            "explore".to_string(),
570            "exp_456".to_string(),
571            "idem_abcdef1234567890".to_string(),
572        );
573        assert!(success.validate().is_ok());
574        assert!(success.ok);
575
576        let error = builders::error(
577            "test error".to_string(),
578            50,
579            "a".repeat(64),
580            "abc123fed".to_string(), // Valid 9-char hex commit hash
581            "test.capability".to_string(),
582            "strict".to_string(),
583            "exp_789".to_string(),
584            "idem_fedcba0987654321".to_string(),
585        );
586        assert!(error.validate().is_ok());
587        assert!(!error.ok);
588        assert!(error.status.contains("error: test error"));
589    }
590
591    #[test]
592    fn test_schema_hash() {
593        let hash1 = ResultSchemaValidator::schema_hash();
594        let hash2 = ResultSchemaValidator::schema_hash();
595
596        // Hash should be deterministic
597        assert_eq!(hash1, hash2);
598
599        // Hash should be valid SHA256 (64 hex chars)
600        assert_eq!(hash1.len(), 64);
601        assert!(hash1.chars().all(|c| c.is_ascii_hexdigit()));
602    }
603
604    #[test]
605    fn test_backward_compatibility_check() {
606        let old_result = create_valid_result();
607        let new_result =
608            create_valid_result().with_meta("new_field".to_string(), json!("new_value"));
609
610        // Adding x_meta fields should be backward compatible
611        assert!(
612            ResultSchemaValidator::check_backward_compatibility(&old_result, &new_result).is_ok()
613        );
614
615        // Same results should be compatible
616        assert!(
617            ResultSchemaValidator::check_backward_compatibility(&old_result, &old_result).is_ok()
618        );
619    }
620
621    #[test]
622    fn test_result_schema_error_display() {
623        let empty_field_error = ResultSchemaError::EmptyRequiredField("status".to_string());
624        let format_error = ResultSchemaError::InvalidFieldFormat {
625            field: "commit".to_string(),
626            expected: "9-character hex string".to_string(),
627            actual: "abc".to_string(),
628        };
629        let unknown_field_error = ResultSchemaError::UnknownField {
630            field: "unknown".to_string(),
631        };
632        let version_error = ResultSchemaError::VersionMismatch {
633            expected: 1,
634            actual: 2,
635        };
636        let deserialization_error =
637            ResultSchemaError::DeserializationFailed("JSON parse error".to_string());
638
639        // Test Display trait
640        assert!(format!("{}", empty_field_error).contains("Empty required field"));
641        assert!(format!("{}", format_error).contains("Invalid format for field commit"));
642        assert!(format!("{}", unknown_field_error).contains("Unknown field detected"));
643        assert!(format!("{}", version_error).contains("Schema version mismatch"));
644        assert!(format!("{}", deserialization_error).contains("Deserialization failed"));
645
646        // Test Debug trait
647        let debug_str = format!("{:?}", empty_field_error);
648        assert!(debug_str.contains("EmptyRequiredField"));
649    }
650
651    #[test]
652    fn test_json_validation_malformed() {
653        let malformed_json = "{ invalid json";
654        let result = ResultSchemaValidator::validate_json(malformed_json);
655        assert!(result.is_err());
656    }
657
658    #[test]
659    fn test_json_validation_missing_required_fields() {
660        let incomplete_json = json!({
661            "ok": true,
662            // Missing required fields like status, latency_ms, bytes, etc.
663        })
664        .to_string();
665
666        let result = ResultSchemaValidator::validate_json(&incomplete_json);
667        assert!(result.is_err());
668    }
669
670    #[test]
671    fn test_validation_invalid_commit_formats() {
672        let invalid_commits = vec![
673            "".to_string(),          // Empty
674            "abc".to_string(),       // Too short (< 7 chars)
675            "a".repeat(41),          // Too long (> 40 chars)
676            "abcdefGHI".to_string(), // Invalid hex character
677            "abcdef12@".to_string(), // Invalid character
678        ];
679
680        for commit in invalid_commits {
681            let mut result = create_valid_result();
682            result.commit = commit.clone();
683
684            let error = result.validate();
685            assert!(error.is_err(), "Commit '{}' should be invalid", commit);
686        }
687    }
688
689    #[test]
690    fn test_validation_invalid_capability_digest_formats() {
691        let invalid_digests = vec![
692            "".to_string(),                 // Empty
693            "a".repeat(63),                 // Too short
694            "a".repeat(65),                 // Too long
695            "g".repeat(64),                 // Invalid hex character
696            format!("{}@", "a".repeat(63)), // Invalid character
697        ];
698
699        for digest in invalid_digests {
700            let mut result = create_valid_result();
701            result.capability_digest = digest.clone();
702
703            let error = result.validate();
704            assert!(
705                error.is_err(),
706                "Capability digest '{}' should be invalid",
707                digest
708            );
709        }
710    }
711
712    #[test]
713    fn test_validation_invalid_exp_id_formats() {
714        let invalid_exp_ids = vec![
715            "", // Empty - only case that actually fails validation
716        ];
717
718        for exp_id in invalid_exp_ids {
719            let mut result = create_valid_result();
720            result.exp_id = exp_id.to_string();
721
722            let error = result.validate();
723            assert!(error.is_err(), "Exp ID '{}' should be invalid", exp_id);
724        }
725    }
726
727    #[test]
728    fn test_validation_invalid_idem_key_formats() {
729        let invalid_idem_keys = vec![
730            "",               // Empty
731            "idem",           // Too short
732            "idem_",          // No key part
733            "invalid_123abc", // Wrong prefix
734            "idem_",          // Just prefix
735            "idem_ghi",       // Non-hex characters after underscore
736        ];
737
738        for idem_key in invalid_idem_keys {
739            let mut result = create_valid_result();
740            result.idem_key = idem_key.to_string();
741
742            let error = result.validate();
743            assert!(error.is_err(), "Idem key '{}' should be invalid", idem_key);
744        }
745    }
746
747    #[test]
748    fn test_validation_invalid_modes() {
749        let invalid_modes = vec![
750            "",             // Empty
751            "invalid",      // Not in valid list
752            "STRICT",       // Wrong case
753            "explore_mode", // Contains underscore
754        ];
755
756        for mode in invalid_modes {
757            let mut result = create_valid_result();
758            result.mode = mode.to_string();
759
760            let error = result.validate();
761            assert!(error.is_err(), "Mode '{}' should be invalid", mode);
762        }
763    }
764
765    #[test]
766    fn test_validation_invalid_layers() {
767        let invalid_layers = vec![
768            "",           // Empty
769            "invalid",    // Not in valid list
770            "ATOM",       // Wrong case
771            "atom_layer", // Contains underscore
772        ];
773
774        for layer in invalid_layers {
775            let mut result = create_valid_result();
776            result.layer = layer.to_string();
777
778            let error = result.validate();
779            assert!(error.is_err(), "Layer '{}' should be invalid", layer);
780        }
781    }
782
783    #[test]
784    fn test_builder_success_with_zero_values() {
785        let result = builders::success(
786            0, // zero latency
787            0, // zero bytes
788            "a".repeat(64),
789            "abc123def".to_string(),
790            "test.capability".to_string(),
791            "strict".to_string(),
792            "exp_test".to_string(),
793            "idem_1234567890abcdef".to_string(),
794        );
795
796        assert!(result.validate().is_ok());
797        assert!(result.ok);
798        assert_eq!(result.latency_ms, 0);
799        assert_eq!(result.bytes, 0);
800    }
801
802    #[test]
803    fn test_builder_error_empty_message() {
804        let result = builders::error(
805            "".to_string(), // Empty error message
806            100,
807            "a".repeat(64),
808            "abc123def".to_string(),
809            "test.capability".to_string(),
810            "strict".to_string(),
811            "exp_test".to_string(),
812            "idem_1234567890abcdef".to_string(),
813        );
814
815        assert!(result.validate().is_ok()); // Empty error message is allowed
816        assert!(!result.ok);
817        assert!(result.status.contains("error:"));
818    }
819
820    #[test]
821    fn test_result_metadata_operations() {
822        let mut result = create_valid_result();
823
824        // Test multiple meta additions
825        result = result
826            .with_meta("key1".to_string(), json!("value1"))
827            .with_meta("key2".to_string(), json!({"nested": "object"}))
828            .with_meta("key3".to_string(), json!([1, 2, 3]));
829
830        assert_eq!(result.get_meta("key1"), Some(&json!("value1")));
831        assert_eq!(result.get_meta("key2"), Some(&json!({"nested": "object"})));
832        assert_eq!(result.get_meta("key3"), Some(&json!([1, 2, 3])));
833        assert_eq!(result.get_meta("nonexistent"), None);
834
835        // Overwrite existing meta
836        result = result.with_meta("key1".to_string(), json!("new_value"));
837        assert_eq!(result.get_meta("key1"), Some(&json!("new_value")));
838    }
839
840    #[test]
841    fn test_json_round_trip_with_complex_meta() {
842        let original = create_valid_result()
843            .with_meta("string".to_string(), json!("test"))
844            .with_meta("number".to_string(), json!(42))
845            .with_meta("boolean".to_string(), json!(true))
846            .with_meta("null".to_string(), json!(null))
847            .with_meta("array".to_string(), json!([1, "two", 3.0]))
848            .with_meta("object".to_string(), json!({"nested": {"deep": "value"}}));
849
850        let json = serde_json::to_string(&original).unwrap();
851        let deserialized = ResultSchemaValidator::validate_json(&json).unwrap();
852
853        assert_eq!(original, deserialized);
854        assert_eq!(deserialized.get_meta("string"), Some(&json!("test")));
855        assert_eq!(deserialized.get_meta("number"), Some(&json!(42)));
856        assert_eq!(deserialized.get_meta("boolean"), Some(&json!(true)));
857        assert_eq!(deserialized.get_meta("null"), Some(&json!(null)));
858        assert_eq!(
859            deserialized.get_meta("array"),
860            Some(&json!([1, "two", 3.0]))
861        );
862        assert_eq!(
863            deserialized.get_meta("object"),
864            Some(&json!({"nested": {"deep": "value"}}))
865        );
866    }
867
868    #[test]
869    fn test_schema_hash_consistency() {
870        // Test that schema hash is consistent across calls
871        let hashes: Vec<String> = (0..10)
872            .map(|_| ResultSchemaValidator::schema_hash())
873            .collect();
874
875        // All hashes should be identical
876        for hash in &hashes {
877            assert_eq!(hash, &hashes[0]);
878        }
879
880        // Should be a valid SHA-256 hash
881        assert_eq!(hashes[0].len(), 64);
882        assert!(hashes[0].chars().all(|c| c.is_ascii_hexdigit()));
883    }
884}