schema.rs (18467B)
1 use radroots_mesh_agent_proto::{ 2 RADROOTS_MESH_AGENT_SCHEMA, RADROOTS_MESH_AGENT_SCHEMA_ID, 3 RADROOTS_MESH_AGENT_SCHEMA_NAMESPACE, RadrootsMeshAgentProtoError, schema_sha256_hex, 4 validate_schema, validate_schema_text, 5 }; 6 7 #[test] 8 fn schema_declares_mesh_agent_v1_surface() { 9 validate_schema().expect("schema validates"); 10 11 assert!(RADROOTS_MESH_AGENT_SCHEMA.contains(RADROOTS_MESH_AGENT_SCHEMA_ID)); 12 assert!(RADROOTS_MESH_AGENT_SCHEMA.contains(RADROOTS_MESH_AGENT_SCHEMA_NAMESPACE)); 13 assert!(RADROOTS_MESH_AGENT_SCHEMA.contains("struct MeshAgentRequest")); 14 assert!(RADROOTS_MESH_AGENT_SCHEMA.contains("struct MeshAgentResponse")); 15 assert!(RADROOTS_MESH_AGENT_SCHEMA.contains("struct MeshAgentStatusRequest")); 16 assert!(RADROOTS_MESH_AGENT_SCHEMA.contains("struct MeshAgentStatusResponse")); 17 assert!(RADROOTS_MESH_AGENT_SCHEMA.contains("struct MeshAgentPublishRequest")); 18 assert!(RADROOTS_MESH_AGENT_SCHEMA.contains("struct MeshAgentPublishResponse")); 19 assert!(RADROOTS_MESH_AGENT_SCHEMA.contains("enum MeshAgentTransportKind")); 20 assert!(RADROOTS_MESH_AGENT_SCHEMA.contains("enum MeshAgentTransportOutcome")); 21 } 22 23 #[test] 24 fn schema_is_canonical_transport_v1_authority() { 25 validate_schema().expect("schema validates"); 26 27 assert_eq!(RADROOTS_MESH_AGENT_SCHEMA_ID, "0xb83e0c4f71838d9a"); 28 assert_ne!(RADROOTS_MESH_AGENT_SCHEMA_ID, "0xdecafbaddecaf001"); 29 assert_eq!( 30 validate_schema_text( 31 RADROOTS_MESH_AGENT_SCHEMA 32 .replace(RADROOTS_MESH_AGENT_SCHEMA_ID, "0xdecafbaddecaf001") 33 .as_str(), 34 ), 35 Err(RadrootsMeshAgentProtoError::MissingSchemaId) 36 ); 37 } 38 39 #[test] 40 fn schema_covers_transport_v1_status_and_publish_intent() { 41 for required in [ 42 "requestId @0 :Text;", 43 "statusRequest @3 :MeshAgentStatusRequest;", 44 "publishRequest @4 :MeshAgentPublishRequest;", 45 "statusResponse @4 :MeshAgentStatusResponse;", 46 "publishResponse @5 :MeshAgentPublishResponse;", 47 "includeTransports @0 :Bool;", 48 "transports @0 :List(MeshAgentTransportStatus);", 49 "transport @0 :MeshAgentTransportKind;", 50 "profileId @1 :Text;", 51 "configured @3 :Bool;", 52 "implementation @4 :MeshAgentImplementation;", 53 "usableForDelivery @5 :Bool;", 54 "message @6 :Text;", 55 "real @0;", 56 "mock @1;", 57 "previewUnavailable @2;", 58 "reticulum @0;", 59 "publishRequestId @0 :Text;", 60 "payloadCbor @1 :Data;", 61 "eventId @2 :Text;", 62 "targetFingerprint @3 :Text;", 63 "transportReceipts @2 :List(MeshAgentTransportReceipt);", 64 "eventId @3 :Text;", 65 "outcome @2 :MeshAgentTransportOutcome;", 66 "deferredUntilImplemented @4;", 67 "transportUnavailable @8;", 68 "errors @3 :List(MeshAgentError);", 69 ] { 70 assert!(RADROOTS_MESH_AGENT_SCHEMA.contains(required), "{required}"); 71 } 72 } 73 74 #[test] 75 fn schema_excludes_retired_transport_status_vocabulary() { 76 validate_schema().expect("schema validates"); 77 78 for forbidden in [ 79 "readiness", 80 "implementationState", 81 "publishUsable", 82 "fetchUsable", 83 "redactedMessage", 84 "previewNoop", 85 ] { 86 assert!( 87 !RADROOTS_MESH_AGENT_SCHEMA.contains(forbidden), 88 "{forbidden}" 89 ); 90 } 91 } 92 93 #[test] 94 fn schema_hash_is_deterministic() { 95 let first = schema_sha256_hex(); 96 let second = schema_sha256_hex(); 97 98 assert_eq!(first, second); 99 assert_eq!( 100 first, 101 "712aaa11dfec25abf44edb3b0be447f0596442271d46a8b1d9fedb7c3df00bb2" 102 ); 103 } 104 105 #[test] 106 fn schema_validator_rejects_missing_required_surface() { 107 assert_eq!( 108 validate_schema_text("@0xb83e0c4f71838d9a;") 109 .expect_err("schema missing namespace and structs"), 110 RadrootsMeshAgentProtoError::MissingNamespace 111 ); 112 } 113 114 #[test] 115 fn schema_validator_reports_each_missing_required_surface() { 116 let valid = RADROOTS_MESH_AGENT_SCHEMA; 117 let cases = [ 118 ( 119 valid.replace(RADROOTS_MESH_AGENT_SCHEMA_ID, "0x0000000000000000"), 120 RadrootsMeshAgentProtoError::MissingSchemaId, 121 ), 122 ( 123 valid.replace(RADROOTS_MESH_AGENT_SCHEMA_NAMESPACE, "radroots::missing"), 124 RadrootsMeshAgentProtoError::MissingNamespace, 125 ), 126 ( 127 valid.replace("struct MeshAgentRequest", "struct MissingMeshAgentRequest"), 128 RadrootsMeshAgentProtoError::MissingRequest, 129 ), 130 ( 131 valid.replace("frameCbor @2 :Data;", "frameBytes @2 :Data;"), 132 RadrootsMeshAgentProtoError::MissingRequest, 133 ), 134 ( 135 valid.replace("enum MeshAgentAction", "enum MissingMeshAgentAction"), 136 RadrootsMeshAgentProtoError::MissingAction, 137 ), 138 ( 139 valid.replace("validateFrame @0;", "validateSomethingElse @0;"), 140 RadrootsMeshAgentProtoError::MissingAction, 141 ), 142 ( 143 valid.replace("stageDelivery @1;", "stageSomethingElse @1;"), 144 RadrootsMeshAgentProtoError::MissingAction, 145 ), 146 ( 147 valid.replace("observeEventHead @2;", "observeSomethingElse @2;"), 148 RadrootsMeshAgentProtoError::MissingAction, 149 ), 150 ( 151 valid.replace("status @3;", "agentStatus @3;"), 152 RadrootsMeshAgentProtoError::MissingAction, 153 ), 154 ( 155 valid.replace("publish @4;", "publishSomethingElse @4;"), 156 RadrootsMeshAgentProtoError::MissingAction, 157 ), 158 ( 159 valid.replace( 160 "struct MeshAgentResponse", 161 "struct MissingMeshAgentResponse", 162 ), 163 RadrootsMeshAgentProtoError::MissingResponse, 164 ), 165 ( 166 valid.replace( 167 "enum MeshAgentResponseStatus", 168 "enum MissingMeshAgentResponseStatus", 169 ), 170 RadrootsMeshAgentProtoError::MissingResponse, 171 ), 172 ( 173 valid.replace("struct MeshAgentReceipt", "struct MissingMeshAgentReceipt"), 174 RadrootsMeshAgentProtoError::MissingReceipt, 175 ), 176 ( 177 valid.replace( 178 "acceptedEventHeads @1 :List(Text);", 179 "acceptedEventIds @1 :List(Text);", 180 ), 181 RadrootsMeshAgentProtoError::MissingReceipt, 182 ), 183 ( 184 valid.replace( 185 "struct MeshAgentStatusRequest", 186 "struct MissingMeshAgentStatusRequest", 187 ), 188 RadrootsMeshAgentProtoError::MissingStatusSurface, 189 ), 190 ( 191 valid.replace( 192 "transports @0 :List(MeshAgentTransportStatus);", 193 "transportStatuses @0 :List(MeshAgentTransportStatus);", 194 ), 195 RadrootsMeshAgentProtoError::MissingStatusSurface, 196 ), 197 ( 198 valid.replace( 199 "transport @0 :MeshAgentTransportKind;", 200 "transportKind @0 :MeshAgentTransportKind;", 201 ), 202 RadrootsMeshAgentProtoError::MissingStatusSurface, 203 ), 204 ( 205 valid.replace("profileId @1 :Text;", "transportProfileId @1 :Text;"), 206 RadrootsMeshAgentProtoError::MissingStatusSurface, 207 ), 208 ( 209 valid.replace("configured @3 :Bool;", "configured @3 :Text;"), 210 RadrootsMeshAgentProtoError::MissingStatusSurface, 211 ), 212 ( 213 valid.replace( 214 "implementation @4 :MeshAgentImplementation;", 215 "implementation @4 :Text;", 216 ), 217 RadrootsMeshAgentProtoError::MissingStatusSurface, 218 ), 219 ( 220 valid.replace("usableForDelivery @5 :Bool;", "publishUsable @5 :Bool;"), 221 RadrootsMeshAgentProtoError::MissingStatusSurface, 222 ), 223 ( 224 valid.replace("message @6 :Text;", "redactedMessage @6 :Text;"), 225 RadrootsMeshAgentProtoError::MissingStatusSurface, 226 ), 227 ( 228 valid.replace("reticulum @0;", "reticulumPreview @0;"), 229 RadrootsMeshAgentProtoError::MissingStatusSurface, 230 ), 231 ( 232 valid.replace("previewUnavailable @2;", "previewUnavailable @3;"), 233 RadrootsMeshAgentProtoError::MissingStatusSurface, 234 ), 235 ( 236 valid.replace( 237 "struct MeshAgentPublishRequest", 238 "struct MissingMeshAgentPublishRequest", 239 ), 240 RadrootsMeshAgentProtoError::MissingPublishSurface, 241 ), 242 ( 243 valid.replace("publishRequestId @0 :Text;", "operationId @0 :Text;"), 244 RadrootsMeshAgentProtoError::MissingPublishSurface, 245 ), 246 ( 247 valid.replace("eventId @2 :Text;", "eventIdentifier @2 :Text;"), 248 RadrootsMeshAgentProtoError::MissingPublishSurface, 249 ), 250 ( 251 valid.replace("targetFingerprint @3 :Text;", "transportTarget @3 :Text;"), 252 RadrootsMeshAgentProtoError::MissingPublishSurface, 253 ), 254 ( 255 valid.replace( 256 "transportReceipts @2 :List(MeshAgentTransportReceipt);", 257 "transportResults @2 :List(MeshAgentTransportReceipt);", 258 ), 259 RadrootsMeshAgentProtoError::MissingPublishSurface, 260 ), 261 ( 262 valid.replace( 263 "outcome @2 :MeshAgentTransportOutcome;", 264 "outcome @2 :Text;", 265 ), 266 RadrootsMeshAgentProtoError::MissingPublishSurface, 267 ), 268 ( 269 valid.replace("message @3 :Text;", "redactedMessage @3 :Text;"), 270 RadrootsMeshAgentProtoError::MissingPublishSurface, 271 ), 272 ( 273 valid.replace( 274 "transportUnavailable @8;", 275 "transportUnavailablePreview @8;", 276 ), 277 RadrootsMeshAgentProtoError::MissingPublishSurface, 278 ), 279 ( 280 valid.replace("struct MeshAgentError", "struct MissingMeshAgentError"), 281 RadrootsMeshAgentProtoError::MissingError, 282 ), 283 ]; 284 285 for (schema, error) in cases { 286 assert_eq!(validate_schema_text(schema.as_str()), Err(error)); 287 } 288 } 289 290 #[test] 291 fn schema_validator_rejects_reintroduced_retired_status_vocabulary() { 292 let valid = RADROOTS_MESH_AGENT_SCHEMA; 293 let cases = [ 294 ( 295 valid.replace( 296 " transports @0 :List(MeshAgentTransportStatus);", 297 " transports @0 :List(MeshAgentTransportStatus);\n readiness @1 :MeshAgentReadinessState;", 298 ), 299 RadrootsMeshAgentProtoError::MissingStatusSurface, 300 ), 301 ( 302 valid.replace( 303 " transports @0 :List(MeshAgentTransportStatus);", 304 " transports @0 :List(MeshAgentTransportStatus);\n implementationState @1 :MeshAgentImplementationState;", 305 ), 306 RadrootsMeshAgentProtoError::MissingStatusSurface, 307 ), 308 ( 309 valid.replace( 310 " message @6 :Text;", 311 " message @6 :Text;\n publishUsable @7 :Bool;", 312 ), 313 RadrootsMeshAgentProtoError::MissingStatusSurface, 314 ), 315 ( 316 valid.replace( 317 " message @6 :Text;", 318 " message @6 :Text;\n fetchUsable @7 :Bool;", 319 ), 320 RadrootsMeshAgentProtoError::MissingStatusSurface, 321 ), 322 ( 323 valid.replace( 324 " message @6 :Text;", 325 " message @6 :Text;\n redactedMessage @7 :Text;", 326 ), 327 RadrootsMeshAgentProtoError::MissingStatusSurface, 328 ), 329 ( 330 valid.replace( 331 " previewUnavailable @2;", 332 " previewUnavailable @2;\n previewNoop @3;", 333 ), 334 RadrootsMeshAgentProtoError::MissingStatusSurface, 335 ), 336 ( 337 valid.replace( 338 "enum MeshAgentTransportKind", 339 "enum MeshAgentReadinessState {\n ready @0;\n}\n\nenum MeshAgentTransportKind", 340 ), 341 RadrootsMeshAgentProtoError::MissingStatusSurface, 342 ), 343 ( 344 valid.replace( 345 " message @3 :Text;", 346 " message @3 :Text;\n redactedMessage @4 :Text;", 347 ), 348 RadrootsMeshAgentProtoError::MissingPublishSurface, 349 ), 350 ]; 351 352 for (schema, error) in cases { 353 assert_eq!(validate_schema_text(schema.as_str()), Err(error)); 354 } 355 } 356 357 #[test] 358 fn schema_validator_rejects_commented_required_declarations() { 359 let schema = 360 RADROOTS_MESH_AGENT_SCHEMA.replace(" frameCbor @2 :Data;", " # frameCbor @2 :Data;"); 361 362 assert_eq!( 363 validate_schema_text(schema.as_str()), 364 Err(RadrootsMeshAgentProtoError::MissingRequest) 365 ); 366 } 367 368 #[test] 369 fn schema_validator_rejects_misplaced_required_declarations() { 370 let schema = RADROOTS_MESH_AGENT_SCHEMA 371 .replace( 372 " acceptedEventHeads @1 :List(Text);", 373 " acceptedEventIds @1 :List(Text);", 374 ) 375 .replace( 376 " message @1 :Text;", 377 " message @1 :Text;\n acceptedEventHeads @2 :List(Text);", 378 ); 379 380 assert_eq!( 381 validate_schema_text(schema.as_str()), 382 Err(RadrootsMeshAgentProtoError::MissingReceipt) 383 ); 384 } 385 386 #[test] 387 fn schema_validator_rejects_duplicate_incompatible_declarations() { 388 let duplicate_ordinal = RADROOTS_MESH_AGENT_SCHEMA.replace( 389 " frameCbor @2 :Data;", 390 " frameCbor @2 :Data;\n frameBytes @2 :Data;", 391 ); 392 let duplicate_name = RADROOTS_MESH_AGENT_SCHEMA.replace( 393 " frameCbor @2 :Data;", 394 " frameCbor @2 :Data;\n frameCbor @9 :Text;", 395 ); 396 397 assert_eq!( 398 validate_schema_text(duplicate_ordinal.as_str()), 399 Err(RadrootsMeshAgentProtoError::InvalidSchema) 400 ); 401 assert_eq!( 402 validate_schema_text(duplicate_name.as_str()), 403 Err(RadrootsMeshAgentProtoError::InvalidSchema) 404 ); 405 } 406 407 #[test] 408 fn schema_validator_rejects_type_drift() { 409 let request_type_drift = 410 RADROOTS_MESH_AGENT_SCHEMA.replace(" frameCbor @2 :Data;", " frameCbor @2 :Text;"); 411 let request_ordinal_drift = 412 RADROOTS_MESH_AGENT_SCHEMA.replace(" frameCbor @2 :Data;", " frameCbor @9 :Data;"); 413 let request_numeric_type_drift = 414 RADROOTS_MESH_AGENT_SCHEMA.replace(" frameCbor @2 :Data;", " frameCbor @2 :123;"); 415 let status_type_drift = RADROOTS_MESH_AGENT_SCHEMA.replace( 416 " includeTransports @0 :Bool;", 417 " includeTransports @0 :Text;", 418 ); 419 420 assert_eq!( 421 validate_schema_text(request_type_drift.as_str()), 422 Err(RadrootsMeshAgentProtoError::MissingRequest) 423 ); 424 assert_eq!( 425 validate_schema_text(request_ordinal_drift.as_str()), 426 Err(RadrootsMeshAgentProtoError::MissingRequest) 427 ); 428 assert_eq!( 429 validate_schema_text(request_numeric_type_drift.as_str()), 430 Err(RadrootsMeshAgentProtoError::MissingRequest) 431 ); 432 assert_eq!( 433 validate_schema_text(status_type_drift.as_str()), 434 Err(RadrootsMeshAgentProtoError::MissingStatusSurface) 435 ); 436 } 437 438 #[test] 439 fn schema_validator_accepts_lexical_trivia_and_ignored_statements() { 440 let decorated = format!( 441 "# hash comment\n// slash comment\n/* *x*/\nusing Escaped = import \"schema\\\\\\\"name\";\n$Other.annotation(\"ignored\");\n;\n{RADROOTS_MESH_AGENT_SCHEMA}" 442 ); 443 444 assert_eq!(validate_schema_text(decorated.as_str()), Ok(())); 445 } 446 447 #[test] 448 fn schema_validator_rejects_malformed_lexical_and_parser_edges() { 449 let malformed = [ 450 "!", 451 "/", 452 "/* unterminated", 453 "\"unterminated", 454 "\"trailing\\", 455 "using Alias", 456 "@;", 457 "@1", 458 "$Cxx.namespace(value);", 459 "struct", 460 "struct A", 461 "struct A { field @x :Text; }", 462 "struct A { field @0 Text; }", 463 "struct A { field @0 :; }", 464 "struct A { field @0 :Text", 465 "struct A { field @0 :\"Text\"; }", 466 "struct A { field @0 :@; }", 467 "enum A { value @0 }", 468 "enum A { value @65536; }", 469 "bogus", 470 "@1; @2;", 471 "$Cxx.namespace(\"a\"); $Cxx.namespace(\"b\");", 472 "struct A {} struct A {}", 473 "enum A {} enum A {}", 474 "enum A { first @0; second @0; }", 475 "enum A { first @0; first @1; }", 476 ]; 477 478 for schema in malformed { 479 assert_eq!( 480 validate_schema_text(schema), 481 Err(RadrootsMeshAgentProtoError::InvalidSchema), 482 "{schema}" 483 ); 484 } 485 } 486 487 #[test] 488 fn schema_validator_ignores_non_namespace_annotations() { 489 for schema in ["#", "$Other;", "$Cxx;", "$Cxx.other;", "$Cxx.namespace;"] { 490 assert_eq!( 491 validate_schema_text(schema), 492 Err(RadrootsMeshAgentProtoError::MissingSchemaId), 493 "{schema}" 494 ); 495 } 496 } 497 498 #[test] 499 fn mesh_agent_proto_errors_have_stable_display_strings() { 500 let cases = [ 501 ( 502 RadrootsMeshAgentProtoError::InvalidSchema, 503 "mesh agent schema is invalid", 504 ), 505 ( 506 RadrootsMeshAgentProtoError::MissingSchemaId, 507 "mesh agent schema id is missing", 508 ), 509 ( 510 RadrootsMeshAgentProtoError::MissingNamespace, 511 "mesh agent schema namespace is missing", 512 ), 513 ( 514 RadrootsMeshAgentProtoError::MissingRequest, 515 "mesh agent request schema is missing", 516 ), 517 ( 518 RadrootsMeshAgentProtoError::MissingAction, 519 "mesh agent action schema is missing", 520 ), 521 ( 522 RadrootsMeshAgentProtoError::MissingResponse, 523 "mesh agent response schema is missing", 524 ), 525 ( 526 RadrootsMeshAgentProtoError::MissingReceipt, 527 "mesh agent receipt schema is missing", 528 ), 529 ( 530 RadrootsMeshAgentProtoError::MissingStatusSurface, 531 "mesh agent status schema surface is missing", 532 ), 533 ( 534 RadrootsMeshAgentProtoError::MissingPublishSurface, 535 "mesh agent publish schema surface is missing", 536 ), 537 ( 538 RadrootsMeshAgentProtoError::MissingError, 539 "mesh agent error schema is missing", 540 ), 541 ]; 542 543 for (error, message) in cases { 544 assert_eq!(error.to_string(), message); 545 } 546 }