1#![allow(missing_docs, clippy::should_implement_trait)]
3
4use serde::{Deserialize, Serialize};
32use soma_som_core::quad::{Quad, Tree};
33
34pub const COMMAND_PREFIX: &str = "command.";
38
39pub const RESULT_PREFIX: &str = "result.";
41
42
43#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
51pub struct RingCommand {
52 pub command_type: String,
54
55 pub payload: String,
57
58 pub actor: String,
64
65 pub role_key: String,
70
71 pub request_id: String,
76}
77
78impl RingCommand {
79 pub fn new(
81 command_type: impl Into<String>,
82 payload: impl Into<String>,
83 actor: impl Into<String>,
84 role_key: impl Into<String>,
85 request_id: impl Into<String>,
86 ) -> Self {
87 Self {
88 command_type: command_type.into(),
89 payload: payload.into(),
90 actor: actor.into(),
91 role_key: role_key.into(),
92 request_id: request_id.into(),
93 }
94 }
95
96 pub fn inject_into(&self, tree: &mut Tree) {
98 tree.insert("command.type".into(), self.command_type.as_bytes().to_vec());
99 tree.insert("command.payload".into(), self.payload.as_bytes().to_vec());
100 tree.insert("command.admin".into(), self.actor.as_bytes().to_vec());
101 tree.insert("command.role".into(), self.role_key.as_bytes().to_vec());
102 tree.insert(
103 "command.request_id".into(),
104 self.request_id.as_bytes().to_vec(),
105 );
106 }
107
108 pub fn extract_from(tree: &Tree) -> Option<Self> {
112 let command_type = tree
113 .get("command.type")
114 .map(|v| String::from_utf8_lossy(v).into_owned())?;
115
116 let payload = tree
117 .get("command.payload")
118 .map(|v| String::from_utf8_lossy(v).into_owned())
119 .unwrap_or_default();
120
121 let actor = tree
122 .get("command.admin")
123 .map(|v| String::from_utf8_lossy(v).into_owned())
124 .unwrap_or_default();
125
126 let role_key = tree
127 .get("command.role")
128 .map(|v| String::from_utf8_lossy(v).into_owned())
129 .unwrap_or_default();
130
131 let request_id = tree
132 .get("command.request_id")
133 .map(|v| String::from_utf8_lossy(v).into_owned())
134 .unwrap_or_default();
135
136 Some(Self {
137 command_type,
138 payload,
139 actor,
140 role_key,
141 request_id,
142 })
143 }
144
145 pub fn is_command(tree: &Tree) -> bool {
147 tree.contains_key("command.type")
148 }
149}
150
151#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
155#[non_exhaustive]
156pub enum CommandStatus {
157 Success,
159 Denied,
161 Error,
163}
164
165impl CommandStatus {
166 pub fn as_str(&self) -> &'static str {
167 match self {
168 CommandStatus::Success => "success",
169 CommandStatus::Denied => "denied",
170 CommandStatus::Error => "error",
171 }
172 }
173
174 pub fn from_str(s: &str) -> Option<Self> {
176 match s {
177 "success" => Some(CommandStatus::Success),
178 "denied" => Some(CommandStatus::Denied),
179 "error" => Some(CommandStatus::Error),
180 _ => None,
181 }
182 }
183}
184
185#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
190pub struct CommandResult {
191 pub status: CommandStatus,
193
194 pub command_type: String,
196
197 pub payload: String,
199
200 pub error: String,
202
203 pub request_id: String,
205}
206
207impl CommandResult {
208 pub fn success(
210 command_type: impl Into<String>,
211 payload: impl Into<String>,
212 request_id: impl Into<String>,
213 ) -> Self {
214 Self {
215 status: CommandStatus::Success,
216 command_type: command_type.into(),
217 payload: payload.into(),
218 error: String::new(),
219 request_id: request_id.into(),
220 }
221 }
222
223 pub fn denied(
225 command_type: impl Into<String>,
226 reason: impl Into<String>,
227 request_id: impl Into<String>,
228 ) -> Self {
229 Self {
230 status: CommandStatus::Denied,
231 command_type: command_type.into(),
232 payload: String::new(),
233 error: reason.into(),
234 request_id: request_id.into(),
235 }
236 }
237
238 pub fn error(
240 command_type: impl Into<String>,
241 error: impl Into<String>,
242 request_id: impl Into<String>,
243 ) -> Self {
244 Self {
245 status: CommandStatus::Error,
246 command_type: command_type.into(),
247 payload: String::new(),
248 error: error.into(),
249 request_id: request_id.into(),
250 }
251 }
252
253 pub fn inject_into(&self, tree: &mut Tree) {
255 tree.insert(
256 "result.status".into(),
257 self.status.as_str().as_bytes().to_vec(),
258 );
259 tree.insert(
260 "result.command_type".into(),
261 self.command_type.as_bytes().to_vec(),
262 );
263 tree.insert("result.payload".into(), self.payload.as_bytes().to_vec());
264 tree.insert("result.error".into(), self.error.as_bytes().to_vec());
265 tree.insert(
266 "result.request_id".into(),
267 self.request_id.as_bytes().to_vec(),
268 );
269 }
270
271 pub fn extract_from(tree: &Tree) -> Option<Self> {
275 let status_str = tree
276 .get("result.status")
277 .map(|v| String::from_utf8_lossy(v).into_owned())?;
278 let status = CommandStatus::from_str(&status_str)?;
279
280 let command_type = tree
281 .get("result.command_type")
282 .map(|v| String::from_utf8_lossy(v).into_owned())
283 .unwrap_or_default();
284
285 let payload = tree
286 .get("result.payload")
287 .map(|v| String::from_utf8_lossy(v).into_owned())
288 .unwrap_or_default();
289
290 let error = tree
291 .get("result.error")
292 .map(|v| String::from_utf8_lossy(v).into_owned())
293 .unwrap_or_default();
294
295 let request_id = tree
296 .get("result.request_id")
297 .map(|v| String::from_utf8_lossy(v).into_owned())
298 .unwrap_or_default();
299
300 Some(Self {
301 status,
302 command_type,
303 payload,
304 error,
305 request_id,
306 })
307 }
308
309 pub fn is_result(tree: &Tree) -> bool {
311 tree.contains_key("result.status")
312 }
313}
314
315pub fn inject_command(quad: &Quad, command: &RingCommand) -> Quad {
334 let mut tree = quad.tree.clone();
335 command.inject_into(&mut tree);
336
337 let root = {
339 let mut hasher = blake3::Hasher::new();
340 hasher.update(b"command_injection");
341 hasher.update(&quad.root);
342 hasher.update(command.command_type.as_bytes());
343 hasher.update(command.request_id.as_bytes());
344 *hasher.finalize().as_bytes()
345 };
346
347 Quad::new(root, quad.pointer, tree)
348}
349
350pub fn validate_payload(
367 schema: &soma_som_core::extension::CommandSchema,
368 payload: &str,
369) -> Result<(), String> {
370 if payload.is_empty() || payload == "{}" {
372 let has_required = schema.fields.iter().any(|f| f.required);
373 if has_required {
374 let missing: Vec<&str> = schema
375 .fields
376 .iter()
377 .filter(|f| f.required)
378 .map(|f| f.name.as_str())
379 .collect();
380 return Err(format!(
381 "payload validation failed for '{}': missing required fields: {}",
382 schema.command_type,
383 missing.join(", ")
384 ));
385 }
386 return Ok(());
387 }
388
389 let value: serde_json::Value = serde_json::from_str(payload).map_err(|e| {
390 format!(
391 "payload validation failed for '{}': invalid JSON: {e}",
392 schema.command_type
393 )
394 })?;
395
396 let obj = match value.as_object() {
397 Some(o) => o,
398 None => {
399 return Err(format!(
400 "payload validation failed for '{}': payload must be a JSON object",
401 schema.command_type
402 ));
403 }
404 };
405
406 let mut errors = Vec::new();
407
408 for field in &schema.fields {
409 match obj.get(&field.name) {
410 None if field.required => {
411 errors.push(format!("missing required field '{}'", field.name));
412 }
413 None => {} Some(val) => {
415 use soma_som_core::extension::SchemaFieldType;
416 let type_ok = match field.field_type {
417 SchemaFieldType::String => val.is_string(),
418 SchemaFieldType::Number => val.is_number(),
419 SchemaFieldType::Boolean => val.is_boolean(),
420 SchemaFieldType::Object => val.is_object(),
421 SchemaFieldType::Array => val.is_array(),
422 _ => false,
425 };
426 if !type_ok {
427 let actual = match val {
428 serde_json::Value::Null => "null",
429 serde_json::Value::Bool(_) => "boolean",
430 serde_json::Value::Number(_) => "number",
431 serde_json::Value::String(_) => "string",
432 serde_json::Value::Array(_) => "array",
433 serde_json::Value::Object(_) => "object",
434 };
435 errors.push(format!(
436 "field '{}' expected type '{}', got '{}'",
437 field.name, field.field_type, actual,
438 ));
439 }
440 }
445 }
446 }
447
448 if errors.is_empty() {
449 Ok(())
450 } else {
451 Err(format!(
452 "payload validation failed for '{}': {}",
453 schema.command_type,
454 errors.join("; ")
455 ))
456 }
457}
458
459pub const VIEW_PREFIX: &str = "view.";
466
467#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
480pub struct ViewIntent {
481 pub view_id: String,
484
485 pub scope: String,
488
489 pub role_key: String,
494
495 pub request_id: String,
498}
499
500impl ViewIntent {
501 pub fn new(
503 view_id: impl Into<String>,
504 scope: impl Into<String>,
505 role_key: impl Into<String>,
506 request_id: impl Into<String>,
507 ) -> Self {
508 Self {
509 view_id: view_id.into(),
510 scope: scope.into(),
511 role_key: role_key.into(),
512 request_id: request_id.into(),
513 }
514 }
515
516 pub fn inject_into(&self, tree: &mut Tree) {
518 tree.insert("view.id".into(), self.view_id.as_bytes().to_vec());
519 tree.insert("view.scope".into(), self.scope.as_bytes().to_vec());
520 tree.insert(
521 "view.requestor_role".into(),
522 self.role_key.as_bytes().to_vec(),
523 );
524 tree.insert(
525 "view.request_id".into(),
526 self.request_id.as_bytes().to_vec(),
527 );
528 }
529
530 pub fn extract_from(tree: &Tree) -> Option<Self> {
534 let view_id = tree
535 .get("view.id")
536 .map(|v| String::from_utf8_lossy(v).into_owned())?;
537
538 let scope = tree
539 .get("view.scope")
540 .map(|v| String::from_utf8_lossy(v).into_owned())
541 .unwrap_or_default();
542
543 let role_key = tree
544 .get("view.requestor_role")
545 .map(|v| String::from_utf8_lossy(v).into_owned())
546 .unwrap_or_default();
547
548 let request_id = tree
549 .get("view.request_id")
550 .map(|v| String::from_utf8_lossy(v).into_owned())
551 .unwrap_or_default();
552
553 Some(Self {
554 view_id,
555 scope,
556 role_key,
557 request_id,
558 })
559 }
560
561 pub fn is_view_intent(tree: &Tree) -> bool {
563 tree.contains_key("view.id")
564 }
565}
566
567pub fn inject_view_intent(quad: &Quad, intent: &ViewIntent) -> Quad {
584 let mut tree = quad.tree.clone();
585 intent.inject_into(&mut tree);
586
587 let root = {
588 let mut hasher = blake3::Hasher::new();
589 hasher.update(b"view_intent_injection");
590 hasher.update(&quad.root);
591 hasher.update(intent.view_id.as_bytes());
592 hasher.update(intent.request_id.as_bytes());
593 *hasher.finalize().as_bytes()
594 };
595
596 Quad::new(root, quad.pointer, tree)
597}
598
599#[cfg(test)]
603mod tests {
604 use super::*;
605 use soma_som_core::quad::Quad;
606
607 #[test]
610 fn ring_command_new() {
611 let cmd = RingCommand::new(
612 "user.create",
613 "{\"username\":\"alice\"}",
614 "admin",
615 "admin",
616 "req-001",
617 );
618 assert_eq!(cmd.command_type, "user.create");
619 assert_eq!(cmd.actor, "admin");
620 assert_eq!(cmd.role_key, "admin");
621 assert_eq!(cmd.request_id, "req-001");
622 }
623
624 #[test]
627 fn command_inject_extract_roundtrip() {
628 let cmd = RingCommand::new(
629 "user.delete",
630 "{\"username\":\"bob\"}",
631 "admin",
632 "admin",
633 "req-002",
634 );
635 let mut tree = Tree::new();
636 cmd.inject_into(&mut tree);
637
638 let extracted = RingCommand::extract_from(&tree).expect("should extract");
639 assert_eq!(extracted, cmd);
640 }
641
642 #[test]
643 fn command_extract_returns_none_without_type() {
644 let tree = Tree::new();
645 assert!(RingCommand::extract_from(&tree).is_none());
646 }
647
648 #[test]
649 fn command_is_command_detection() {
650 let mut tree = Tree::new();
651 assert!(!RingCommand::is_command(&tree));
652 tree.insert("command.type".into(), b"user.list".to_vec());
653 assert!(RingCommand::is_command(&tree));
654 }
655
656 #[test]
659 fn inject_command_preserves_existing_tree() {
660 let mut tree = Tree::new();
661 tree.insert("existing.key".into(), b"value".to_vec());
662 let quad = Quad::from_strings("root", "ptr", tree);
663
664 let cmd = RingCommand::new("user.list", "{}", "admin", "admin", "req-003");
665 let injected = inject_command(&quad, &cmd);
666
667 assert_eq!(
668 injected.tree.get("existing.key"),
669 Some(&b"value".to_vec()),
670 "existing keys must be preserved"
671 );
672 assert!(injected.tree.contains_key("command.type"));
673 }
674
675 #[test]
676 fn inject_command_recomputes_root() {
677 let quad = Quad::from_strings("root", "ptr", Tree::new());
678 let cmd = RingCommand::new("user.create", "{}", "admin", "admin", "req-004");
679 let injected = inject_command(&quad, &cmd);
680
681 assert_ne!(
682 injected.root, quad.root,
683 "root must change after command injection"
684 );
685 }
686
687 #[test]
688 fn inject_command_different_commands_different_roots() {
689 let quad = Quad::from_strings("root", "ptr", Tree::new());
690 let cmd_a = RingCommand::new("user.create", "{}", "admin", "admin", "req-a");
691 let cmd_b = RingCommand::new("user.delete", "{}", "admin", "admin", "req-b");
692
693 let injected_a = inject_command(&quad, &cmd_a);
694 let injected_b = inject_command(&quad, &cmd_b);
695
696 assert_ne!(
697 injected_a.root, injected_b.root,
698 "different commands must produce different roots"
699 );
700 }
701
702 #[test]
703 fn inject_command_preserves_pointer() {
704 let quad = Quad::from_strings("root", "ptr", Tree::new());
705 let cmd = RingCommand::new("user.list", "{}", "admin", "admin", "req-005");
706 let injected = inject_command(&quad, &cmd);
707
708 assert_eq!(injected.pointer, quad.pointer, "pointer must be preserved");
709 }
710
711 #[test]
714 fn result_success() {
715 let r = CommandResult::success("user.create", "{\"username\":\"alice\"}", "req-001");
716 assert_eq!(r.status, CommandStatus::Success);
717 assert_eq!(r.command_type, "user.create");
718 assert!(!r.payload.is_empty());
719 assert!(r.error.is_empty());
720 }
721
722 #[test]
723 fn result_denied() {
724 let r = CommandResult::denied("user.delete", "insufficient privileges", "req-002");
725 assert_eq!(r.status, CommandStatus::Denied);
726 assert!(r.payload.is_empty());
727 assert_eq!(r.error, "insufficient privileges");
728 }
729
730 #[test]
731 fn result_error() {
732 let r = CommandResult::error("user.create", "username already exists", "req-003");
733 assert_eq!(r.status, CommandStatus::Error);
734 assert_eq!(r.error, "username already exists");
735 }
736
737 #[test]
740 fn result_inject_extract_roundtrip() {
741 let r = CommandResult::success("user.list", "[{\"username\":\"admin\"}]", "req-004");
742 let mut tree = Tree::new();
743 r.inject_into(&mut tree);
744
745 let extracted = CommandResult::extract_from(&tree).expect("should extract");
746 assert_eq!(extracted, r);
747 }
748
749 #[test]
750 fn result_extract_returns_none_without_status() {
751 let tree = Tree::new();
752 assert!(CommandResult::extract_from(&tree).is_none());
753 }
754
755 #[test]
756 fn result_is_result_detection() {
757 let mut tree = Tree::new();
758 assert!(!CommandResult::is_result(&tree));
759 tree.insert("result.status".into(), b"success".to_vec());
760 assert!(CommandResult::is_result(&tree));
761 }
762
763 #[test]
764 fn result_denied_roundtrip() {
765 let r = CommandResult::denied("user.delete", "not authorized", "req-005");
766 let mut tree = Tree::new();
767 r.inject_into(&mut tree);
768
769 let extracted = CommandResult::extract_from(&tree).unwrap();
770 assert_eq!(extracted.status, CommandStatus::Denied);
771 assert_eq!(extracted.error, "not authorized");
772 }
773
774 #[test]
775 fn result_error_roundtrip() {
776 let r = CommandResult::error("user.create", "db write failed", "req-006");
777 let mut tree = Tree::new();
778 r.inject_into(&mut tree);
779
780 let extracted = CommandResult::extract_from(&tree).unwrap();
781 assert_eq!(extracted.status, CommandStatus::Error);
782 assert_eq!(extracted.error, "db write failed");
783 }
784
785 #[test]
788 fn command_status_roundtrip() {
789 for status in [
790 CommandStatus::Success,
791 CommandStatus::Denied,
792 CommandStatus::Error,
793 ] {
794 let parsed = CommandStatus::from_str(status.as_str()).unwrap();
795 assert_eq!(parsed, status);
796 }
797 }
798
799 #[test]
800 fn command_status_unknown_returns_none() {
801 assert!(CommandStatus::from_str("unknown").is_none());
802 assert!(CommandStatus::from_str("").is_none());
803 }
804
805 #[test]
808 fn ring_command_serde_roundtrip() {
809 let cmd = RingCommand::new(
810 "user.create",
811 "{\"username\":\"carol\"}",
812 "admin",
813 "admin",
814 "req-007",
815 );
816 let json = serde_json::to_string(&cmd).unwrap();
817 let decoded: RingCommand = serde_json::from_str(&json).unwrap();
818 assert_eq!(decoded, cmd);
819 }
820
821 #[test]
822 fn command_result_serde_roundtrip() {
823 let r = CommandResult::success("user.list", "[]", "req-008");
824 let json = serde_json::to_string(&r).unwrap();
825 let decoded: CommandResult = serde_json::from_str(&json).unwrap();
826 assert_eq!(decoded, r);
827 }
828
829 #[test]
832 fn command_keys_use_command_prefix() {
833 let cmd = RingCommand::new("user.create", "{}", "admin", "admin", "req-009");
834 let mut tree = Tree::new();
835 cmd.inject_into(&mut tree);
836
837 for key in tree.keys() {
838 assert!(
839 key.starts_with(COMMAND_PREFIX),
840 "command key '{key}' must start with '{COMMAND_PREFIX}'"
841 );
842 }
843 }
844
845 #[test]
846 fn result_keys_use_result_prefix() {
847 let r = CommandResult::success("user.create", "{}", "req-010");
848 let mut tree = Tree::new();
849 r.inject_into(&mut tree);
850
851 for key in tree.keys() {
852 assert!(
853 key.starts_with(RESULT_PREFIX),
854 "result key '{key}' must start with '{RESULT_PREFIX}'"
855 );
856 }
857 }
858
859 #[test]
862 fn command_and_event_namespaces_do_not_collide() {
863 let mut tree = Tree::new();
864
865 tree.insert("event.type".into(), b"login_attempt".to_vec());
867 tree.insert("event.source".into(), b"web".to_vec());
868
869 let cmd = RingCommand::new("user.list", "{}", "admin", "admin", "req-011");
871 cmd.inject_into(&mut tree);
872
873 assert_eq!(tree.get("event.type"), Some(&b"login_attempt".to_vec()),);
875 assert_eq!(tree.get("command.type"), Some(&b"user.list".to_vec()),);
876
877 assert!(RingCommand::extract_from(&tree).is_some());
879 }
880
881 use soma_som_core::extension::{CommandSchema, SchemaField, SchemaFieldType};
884
885 fn user_create_schema() -> CommandSchema {
886 CommandSchema::new("user.create")
887 .field(SchemaField::required("username", SchemaFieldType::String))
888 .field(SchemaField::required("password", SchemaFieldType::String))
889 .field(SchemaField::optional("role", SchemaFieldType::String))
890 }
891
892 #[test]
893 fn validate_valid_payload() {
894 let schema = user_create_schema();
895 let payload = r#"{"username":"alice","password":"secret"}"#;
896 assert!(super::validate_payload(&schema, payload).is_ok());
897 }
898
899 #[test]
900 fn validate_valid_payload_with_extra_fields() {
901 let schema = user_create_schema();
902 let payload = r#"{"username":"alice","password":"secret","extra":"ignored"}"#;
903 assert!(super::validate_payload(&schema, payload).is_ok());
904 }
905
906 #[test]
907 fn validate_missing_required_field() {
908 let schema = user_create_schema();
909 let payload = r#"{"password":"secret"}"#;
910 let err = super::validate_payload(&schema, payload).unwrap_err();
911 assert!(err.contains("missing required field 'username'"));
912 }
913
914 #[test]
915 fn validate_wrong_field_type() {
916 let schema = CommandSchema::new("test.cmd")
917 .field(SchemaField::required("timeout", SchemaFieldType::Number));
918 let payload = r#"{"timeout":"not-a-number"}"#;
919 let err = super::validate_payload(&schema, payload).unwrap_err();
920 assert!(err.contains("expected type 'number'"));
921 assert!(err.contains("got 'string'"));
922 }
923
924 #[test]
925 fn validate_no_schema_fields_accepts_anything() {
926 let schema = CommandSchema::new("user.list");
927 assert!(super::validate_payload(&schema, "{}").is_ok());
928 assert!(super::validate_payload(&schema, r#"{"any":"thing"}"#).is_ok());
929 }
930
931 #[test]
932 fn validate_empty_payload_with_no_required_fields() {
933 let schema = CommandSchema::new("test.cmd")
934 .field(SchemaField::optional("debug", SchemaFieldType::Boolean));
935 assert!(super::validate_payload(&schema, "{}").is_ok());
936 assert!(super::validate_payload(&schema, "").is_ok());
937 }
938
939 #[test]
940 fn validate_empty_payload_with_required_fields_fails() {
941 let schema = user_create_schema();
942 let err = super::validate_payload(&schema, "{}").unwrap_err();
943 assert!(err.contains("missing required fields"));
944 }
945
946 #[test]
947 fn validate_invalid_json_fails() {
948 let schema = user_create_schema();
949 let err = super::validate_payload(&schema, "not json").unwrap_err();
950 assert!(err.contains("invalid JSON"));
951 }
952
953 #[test]
954 fn validate_non_object_json_fails() {
955 let schema = user_create_schema();
956 let err = super::validate_payload(&schema, r#""just a string""#).unwrap_err();
957 assert!(err.contains("must be a JSON object"));
958 }
959
960 #[test]
961 fn validate_optional_field_type_checked_when_present() {
962 let schema = CommandSchema::new("test.cmd")
963 .field(SchemaField::optional("count", SchemaFieldType::Number));
964 assert!(super::validate_payload(&schema, "{}").is_ok());
966 assert!(super::validate_payload(&schema, r#"{"count":42}"#).is_ok());
968 let err = super::validate_payload(&schema, r#"{"count":"nope"}"#).unwrap_err();
970 assert!(err.contains("expected type 'number'"));
971 }
972
973 #[test]
974 fn validate_all_field_types() {
975 let schema = CommandSchema::new("test.types")
976 .field(SchemaField::required("s", SchemaFieldType::String))
977 .field(SchemaField::required("n", SchemaFieldType::Number))
978 .field(SchemaField::required("b", SchemaFieldType::Boolean))
979 .field(SchemaField::required("o", SchemaFieldType::Object))
980 .field(SchemaField::required("a", SchemaFieldType::Array));
981
982 let payload = r#"{"s":"x","n":1,"b":true,"o":{},"a":[]}"#;
983 assert!(super::validate_payload(&schema, payload).is_ok());
984 }
985
986 #[test]
989 fn view_intent_new() {
990 let intent = ViewIntent::new("organ.mirror", "health", "admin", "req-001");
991 assert_eq!(intent.view_id, "organ.mirror");
992 assert_eq!(intent.scope, "health");
993 assert_eq!(intent.role_key, "admin");
994 assert_eq!(intent.request_id, "req-001");
995 }
996
997 #[test]
998 fn view_intent_inject_extract_roundtrip() {
999 let intent = ViewIntent::new(
1000 "term.fu.data.tree",
1001 "identity",
1002 "viewer",
1003 "req-002",
1004 );
1005 let mut tree = Tree::new();
1006 intent.inject_into(&mut tree);
1007
1008 let extracted = ViewIntent::extract_from(&tree).expect("should extract");
1009 assert_eq!(extracted, intent);
1010 }
1011
1012 #[test]
1013 fn view_intent_extract_returns_none_without_id() {
1014 let tree = Tree::new();
1015 assert!(ViewIntent::extract_from(&tree).is_none());
1016 }
1017
1018 #[test]
1019 fn view_intent_is_view_intent_detection() {
1020 let mut tree = Tree::new();
1021 assert!(!ViewIntent::is_view_intent(&tree));
1022 tree.insert("view.id".into(), b"organ.mirror".to_vec());
1023 assert!(ViewIntent::is_view_intent(&tree));
1024 }
1025
1026 #[test]
1027 fn inject_view_intent_preserves_existing_tree() {
1028 let mut tree = Tree::new();
1029 tree.insert("existing.key".into(), b"value".to_vec());
1030 let quad = Quad::from_strings("root", "ptr", tree);
1031
1032 let intent = ViewIntent::new("organ.guard", "policy", "admin", "req-003");
1033 let injected = inject_view_intent(&quad, &intent);
1034
1035 assert_eq!(
1036 injected.tree.get("existing.key"),
1037 Some(&b"value".to_vec()),
1038 "existing keys must be preserved"
1039 );
1040 assert!(injected.tree.contains_key("view.id"));
1041 }
1042
1043 #[test]
1044 fn inject_view_intent_recomputes_root() {
1045 let quad = Quad::from_strings("root", "ptr", Tree::new());
1046 let intent = ViewIntent::new("organ.store", "data", "admin", "req-004");
1047 let injected = inject_view_intent(&quad, &intent);
1048
1049 assert_ne!(
1050 injected.root, quad.root,
1051 "root must change after view intent injection"
1052 );
1053 }
1054
1055 #[test]
1056 fn inject_view_intent_different_intents_different_roots() {
1057 let quad = Quad::from_strings("root", "ptr", Tree::new());
1058 let intent_a = ViewIntent::new("organ.mirror", "health", "admin", "req-a");
1059 let intent_b = ViewIntent::new("organ.guard", "policy", "admin", "req-b");
1060
1061 let injected_a = inject_view_intent(&quad, &intent_a);
1062 let injected_b = inject_view_intent(&quad, &intent_b);
1063
1064 assert_ne!(
1065 injected_a.root, injected_b.root,
1066 "different view intents must produce different roots"
1067 );
1068 }
1069
1070 #[test]
1071 fn inject_view_intent_preserves_pointer() {
1072 let quad = Quad::from_strings("root", "ptr", Tree::new());
1073 let intent = ViewIntent::new("organ.wall", "perimeter", "admin", "req-005");
1074 let injected = inject_view_intent(&quad, &intent);
1075 assert_eq!(injected.pointer, quad.pointer, "pointer must be preserved");
1076 }
1077
1078 #[test]
1079 fn view_intent_serde_roundtrip() {
1080 let intent = ViewIntent::new(
1081 "term.mu.data.tree",
1082 "observability",
1083 "operator",
1084 "req-006",
1085 );
1086 let json = serde_json::to_string(&intent).unwrap();
1087 let decoded: ViewIntent = serde_json::from_str(&json).unwrap();
1088 assert_eq!(decoded, intent);
1089 }
1090
1091 #[test]
1092 fn view_intent_keys_use_view_prefix() {
1093 let intent = ViewIntent::new("organ.mirror", "health", "admin", "req-007");
1094 let mut tree = Tree::new();
1095 intent.inject_into(&mut tree);
1096
1097 for key in tree.keys() {
1098 assert!(
1099 key.starts_with(VIEW_PREFIX),
1100 "view key '{key}' must start with '{VIEW_PREFIX}'"
1101 );
1102 }
1103 }
1104
1105 #[test]
1108 fn view_and_command_namespaces_do_not_collide() {
1109 let mut tree = Tree::new();
1110
1111 tree.insert("event.type".into(), b"login_attempt".to_vec());
1113
1114 let cmd = RingCommand::new("user.list", "{}", "admin", "admin", "req-cmd");
1116 cmd.inject_into(&mut tree);
1117
1118 let r = CommandResult::success("user.list", "[]", "req-cmd");
1120 r.inject_into(&mut tree);
1121
1122 let intent = ViewIntent::new("organ.mirror", "health", "admin", "req-view");
1124 intent.inject_into(&mut tree);
1125
1126 assert_eq!(tree.get("event.type"), Some(&b"login_attempt".to_vec()));
1128 assert_eq!(tree.get("command.type"), Some(&b"user.list".to_vec()));
1129 assert_eq!(tree.get("result.status"), Some(&b"success".to_vec()));
1130 assert_eq!(tree.get("view.id"), Some(&b"organ.mirror".to_vec()));
1131
1132 assert!(RingCommand::is_command(&tree));
1134 assert!(CommandResult::is_result(&tree));
1135 assert!(ViewIntent::is_view_intent(&tree));
1136
1137 let xcmd = RingCommand::extract_from(&tree).expect("cmd");
1139 let xres = CommandResult::extract_from(&tree).expect("res");
1140 let xview = ViewIntent::extract_from(&tree).expect("view");
1141
1142 assert_eq!(xcmd.command_type, "user.list");
1143 assert_eq!(xres.command_type, "user.list");
1144 assert_eq!(xview.view_id, "organ.mirror");
1145
1146 assert_eq!(xcmd.request_id, "req-cmd");
1148 assert_eq!(xres.request_id, "req-cmd");
1149 assert_eq!(xview.request_id, "req-view");
1150 }
1151
1152 #[test]
1153 fn view_intent_does_not_appear_as_command() {
1154 let mut tree = Tree::new();
1155 let intent = ViewIntent::new("organ.mirror", "health", "admin", "req-only-view");
1156 intent.inject_into(&mut tree);
1157
1158 assert!(!RingCommand::is_command(&tree));
1160 assert!(!CommandResult::is_result(&tree));
1161 assert!(ViewIntent::is_view_intent(&tree));
1162
1163 assert!(RingCommand::extract_from(&tree).is_none());
1164 assert!(CommandResult::extract_from(&tree).is_none());
1165 assert!(ViewIntent::extract_from(&tree).is_some());
1166 }
1167
1168 #[test]
1169 fn view_prefix_constant_matches_key_layout() {
1170 assert_eq!(VIEW_PREFIX, "view.");
1172 let intent = ViewIntent::new("organ.mirror", "s", "r", "i");
1173 let mut tree = Tree::new();
1174 intent.inject_into(&mut tree);
1175
1176 for key in tree.keys() {
1177 assert!(
1178 key.starts_with(VIEW_PREFIX),
1179 "expected prefix '{VIEW_PREFIX}' on key '{key}'"
1180 );
1181 }
1182 assert_ne!(VIEW_PREFIX, COMMAND_PREFIX);
1184 assert_ne!(VIEW_PREFIX, RESULT_PREFIX);
1185 }
1186}