1use std::collections::{HashMap, HashSet};
17use std::path::PathBuf;
18use std::sync::Arc;
19use std::time::{Instant, SystemTime};
20
21use petgraph::Direction;
22use petgraph::algo::is_cyclic_directed;
23use petgraph::graph::{DiGraph, NodeIndex};
24use petgraph::visit::EdgeRef;
25use serde::{Deserialize, Serialize};
26use tokio::sync::RwLock;
27
28pub struct ThreeStateModel {
30 pub application_state: Arc<ApplicationState>,
32 pub operation_state: Arc<OperationState>,
34 pub dependency_state: Arc<DependencyState>,
36}
37
38impl ThreeStateModel {
39 pub fn new() -> Self {
41 Self {
42 application_state: Arc::new(ApplicationState::new()),
43 operation_state: Arc::new(OperationState::new()),
44 dependency_state: Arc::new(DependencyState::new()),
45 }
46 }
47
48 pub async fn validate_operation(&self, op: &StateModelProposedOperation) -> ValidationResult {
50 let mut errors = Vec::new();
51 let mut warnings = Vec::new();
52
53 for resource in &op.resources_needed {
55 if !self.application_state.resource_exists(resource).await {
56 warnings.push(format!("Resource '{}' does not exist yet", resource));
58 }
59 }
60
61 let active_ops = self.operation_state.get_active_operations().await;
63 for active_op in active_ops {
64 let active_resources: HashSet<_> = active_op
66 .resources_needed
67 .iter()
68 .chain(active_op.resources_produced.iter())
69 .collect();
70
71 let proposed_resources: HashSet<_> = op
72 .resources_needed
73 .iter()
74 .chain(op.resources_produced.iter())
75 .collect();
76
77 let overlap: Vec<_> = active_resources.intersection(&proposed_resources).collect();
78
79 if !overlap.is_empty() {
80 errors.push(format!(
81 "Conflict with running operation '{}': shared resources {:?}",
82 active_op.id,
83 overlap.iter().map(|s| s.as_str()).collect::<Vec<_>>()
84 ));
85 }
86 }
87
88 if self
90 .dependency_state
91 .would_deadlock(&op.agent_id, &op.resources_needed)
92 .await
93 {
94 errors.push("Operation would create a deadlock".to_string());
95 }
96
97 ValidationResult {
98 valid: errors.is_empty(),
99 errors,
100 warnings,
101 }
102 }
103
104 pub async fn record_state_change(&self, change: StateChange) {
106 for app_change in &change.application_changes {
108 match app_change {
109 ApplicationChange::FileModified { path, new_hash } => {
110 self.application_state
111 .update_file(path.clone(), new_hash.clone())
112 .await;
113 }
114 ApplicationChange::ArtifactInvalidated { artifact_id } => {
115 self.application_state
116 .invalidate_artifact(artifact_id)
117 .await;
118 }
119 ApplicationChange::GitStateChanged { new_state } => {
120 self.application_state
121 .update_git_state(new_state.clone())
122 .await;
123 }
124 ApplicationChange::ResourceCreated { resource_id } => {
125 self.application_state
126 .mark_resource_exists(resource_id)
127 .await;
128 }
129 ApplicationChange::ResourceDeleted { resource_id } => {
130 self.application_state
131 .mark_resource_deleted(resource_id)
132 .await;
133 }
134 }
135 }
136
137 for (from, to, edge) in &change.new_dependencies {
139 self.dependency_state
140 .add_dependency(from, to, edge.clone())
141 .await;
142 }
143 }
144
145 pub async fn snapshot(&self) -> StateSnapshot {
147 StateSnapshot {
148 files: self.application_state.get_all_files().await,
149 locks: self.dependency_state.get_current_holders().await,
150 git_state: self.application_state.get_git_state().await,
151 active_operations: self.operation_state.get_active_operation_ids().await,
152 }
153 }
154}
155
156impl Default for ThreeStateModel {
157 fn default() -> Self {
158 Self::new()
159 }
160}
161
162pub struct ApplicationState {
168 files: RwLock<HashMap<PathBuf, FileStatus>>,
170 build_artifacts: RwLock<HashMap<String, ArtifactStatus>>,
172 git_state: RwLock<GitState>,
174 resources: RwLock<HashSet<String>>,
176}
177
178impl ApplicationState {
179 pub fn new() -> Self {
181 Self {
182 files: RwLock::new(HashMap::new()),
183 build_artifacts: RwLock::new(HashMap::new()),
184 git_state: RwLock::new(GitState::default()),
185 resources: RwLock::new(HashSet::new()),
186 }
187 }
188
189 pub async fn resource_exists(&self, resource_id: &str) -> bool {
191 let path = PathBuf::from(resource_id);
193 if self.files.read().await.contains_key(&path) {
194 return true;
195 }
196
197 self.resources.read().await.contains(resource_id)
199 }
200
201 pub async fn mark_resource_exists(&self, resource_id: &str) {
203 self.resources.write().await.insert(resource_id.to_string());
204 }
205
206 pub async fn mark_resource_deleted(&self, resource_id: &str) {
208 self.resources.write().await.remove(resource_id);
209 }
210
211 pub async fn update_file(&self, path: PathBuf, content_hash: String) {
213 let mut files = self.files.write().await;
214 let status = files.entry(path).or_insert_with(|| FileStatus {
215 exists: true,
216 content_hash: String::new(),
217 last_modified: SystemTime::now(),
218 locked_by: None,
219 dirty: false,
220 });
221 status.content_hash = content_hash;
222 status.last_modified = SystemTime::now();
223 status.dirty = true;
224 status.exists = true;
225 }
226
227 pub async fn get_all_files(&self) -> HashMap<PathBuf, FileStatus> {
229 self.files.read().await.clone()
230 }
231
232 pub async fn invalidate_artifact(&self, artifact_id: &str) {
234 if let Some(artifact) = self.build_artifacts.write().await.get_mut(artifact_id) {
235 artifact.valid = false;
236 }
237 }
238
239 pub async fn update_git_state(&self, state: GitState) {
241 *self.git_state.write().await = state;
242 }
243
244 pub async fn get_git_state(&self) -> GitState {
246 self.git_state.read().await.clone()
247 }
248
249 pub async fn lock_file(&self, path: &PathBuf, agent_id: &str) {
251 if let Some(file) = self.files.write().await.get_mut(path) {
252 file.locked_by = Some(agent_id.to_string());
253 }
254 }
255
256 pub async fn unlock_file(&self, path: &PathBuf) {
258 if let Some(file) = self.files.write().await.get_mut(path) {
259 file.locked_by = None;
260 }
261 }
262
263 pub async fn mark_files_clean(&self) {
265 for file in self.files.write().await.values_mut() {
266 file.dirty = false;
267 }
268 }
269
270 pub async fn record_artifact(&self, artifact_id: String, source_hash: String) {
272 self.build_artifacts.write().await.insert(
273 artifact_id,
274 ArtifactStatus {
275 valid: true,
276 built_from_hash: source_hash,
277 build_time: Instant::now(),
278 },
279 );
280 }
281}
282
283impl Default for ApplicationState {
284 fn default() -> Self {
285 Self::new()
286 }
287}
288
289#[derive(Debug, Clone, Serialize, Deserialize)]
291pub struct FileStatus {
292 pub exists: bool,
294 pub content_hash: String,
296 #[serde(skip, default = "default_system_time")]
298 pub last_modified: SystemTime,
299 pub locked_by: Option<String>,
301 pub dirty: bool,
303}
304
305fn default_system_time() -> SystemTime {
306 SystemTime::UNIX_EPOCH
307}
308
309#[derive(Debug, Clone)]
311pub struct ArtifactStatus {
312 pub valid: bool,
314 pub built_from_hash: String,
316 pub build_time: Instant,
318}
319
320#[derive(Debug, Clone, Default, Serialize, Deserialize)]
322pub struct GitState {
323 pub current_branch: String,
325 pub head_commit: String,
327 pub staged_files: Vec<String>,
329 pub modified_files: Vec<String>,
331 pub has_conflicts: bool,
333}
334
335pub struct OperationState {
341 operations: RwLock<HashMap<String, OperationLog>>,
343 agent_operations: RwLock<HashMap<String, Vec<String>>>,
345 active_operations: RwLock<HashSet<String>>,
347 next_id: RwLock<u64>,
349}
350
351impl OperationState {
352 pub fn new() -> Self {
354 Self {
355 operations: RwLock::new(HashMap::new()),
356 agent_operations: RwLock::new(HashMap::new()),
357 active_operations: RwLock::new(HashSet::new()),
358 next_id: RwLock::new(1),
359 }
360 }
361
362 pub async fn generate_id(&self) -> String {
364 let mut id = self.next_id.write().await;
365 let op_id = format!("op-{}", *id);
366 *id += 1;
367 op_id
368 }
369
370 pub async fn start_operation(&self, log: OperationLog) -> String {
372 let id = log.id.clone();
373
374 self.operations
376 .write()
377 .await
378 .insert(id.clone(), log.clone());
379
380 self.agent_operations
382 .write()
383 .await
384 .entry(log.agent_id.clone())
385 .or_default()
386 .push(id.clone());
387
388 self.active_operations.write().await.insert(id.clone());
390
391 id
392 }
393
394 pub async fn complete_operation(
396 &self,
397 operation_id: &str,
398 success: bool,
399 outputs: Option<serde_json::Value>,
400 error: Option<String>,
401 ) {
402 self.active_operations.write().await.remove(operation_id);
404
405 if let Some(op) = self.operations.write().await.get_mut(operation_id) {
407 op.completed_at = Some(Instant::now());
408 op.status = if success {
409 OperationLogStatus::Completed
410 } else {
411 OperationLogStatus::Failed
412 };
413 op.outputs = outputs;
414 op.error = error;
415 }
416 }
417
418 pub async fn mark_compensated(&self, operation_id: &str) {
420 if let Some(op) = self.operations.write().await.get_mut(operation_id) {
421 op.status = OperationLogStatus::Compensated;
422 }
423 }
424
425 pub async fn get_active_operations(&self) -> Vec<OperationLog> {
427 let active_ids = self.active_operations.read().await.clone();
428 let operations = self.operations.read().await;
429
430 active_ids
431 .iter()
432 .filter_map(|id| operations.get(id).cloned())
433 .collect()
434 }
435
436 pub async fn get_active_operation_ids(&self) -> Vec<String> {
438 self.active_operations
439 .read()
440 .await
441 .iter()
442 .cloned()
443 .collect()
444 }
445
446 pub async fn get_operation(&self, operation_id: &str) -> Option<OperationLog> {
448 self.operations.read().await.get(operation_id).cloned()
449 }
450
451 pub async fn get_agent_operations(&self, agent_id: &str) -> Vec<OperationLog> {
453 let op_ids = self
454 .agent_operations
455 .read()
456 .await
457 .get(agent_id)
458 .cloned()
459 .unwrap_or_default();
460
461 let operations = self.operations.read().await;
462 op_ids
463 .iter()
464 .filter_map(|id| operations.get(id).cloned())
465 .collect()
466 }
467
468 pub async fn add_child_operation(&self, parent_id: &str, child_id: &str) {
470 let mut operations = self.operations.write().await;
471
472 if let Some(parent) = operations.get_mut(parent_id) {
473 parent.child_operations.push(child_id.to_string());
474 }
475
476 if let Some(child) = operations.get_mut(child_id) {
477 child.parent_operation = Some(parent_id.to_string());
478 }
479 }
480}
481
482impl Default for OperationState {
483 fn default() -> Self {
484 Self::new()
485 }
486}
487
488#[derive(Debug, Clone)]
490pub struct OperationLog {
491 pub id: String,
493 pub agent_id: String,
495 pub operation_type: String,
497 pub started_at: Instant,
499 pub completed_at: Option<Instant>,
501 pub status: OperationLogStatus,
503 pub inputs: serde_json::Value,
505 pub outputs: Option<serde_json::Value>,
507 pub error: Option<String>,
509 pub child_operations: Vec<String>,
511 pub parent_operation: Option<String>,
513 pub resources_needed: Vec<String>,
515 pub resources_produced: Vec<String>,
517}
518
519impl OperationLog {
520 pub fn new(
522 id: String,
523 agent_id: String,
524 operation_type: String,
525 inputs: serde_json::Value,
526 ) -> Self {
527 Self {
528 id,
529 agent_id,
530 operation_type,
531 started_at: Instant::now(),
532 completed_at: None,
533 status: OperationLogStatus::Running,
534 inputs,
535 outputs: None,
536 error: None,
537 child_operations: Vec::new(),
538 parent_operation: None,
539 resources_needed: Vec::new(),
540 resources_produced: Vec::new(),
541 }
542 }
543
544 pub fn with_resources(mut self, needed: Vec<String>, produced: Vec<String>) -> Self {
546 self.resources_needed = needed;
547 self.resources_produced = produced;
548 self
549 }
550
551 pub fn duration(&self) -> Option<std::time::Duration> {
553 self.completed_at
554 .map(|end| end.duration_since(self.started_at))
555 }
556}
557
558#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
560pub enum OperationLogStatus {
561 Pending,
563 Running,
565 Completed,
567 Failed,
569 Compensated,
571}
572
573pub struct DependencyState {
579 graph: RwLock<DiGraph<ResourceNode, DependencyEdge>>,
581 resource_index: RwLock<HashMap<String, NodeIndex>>,
583}
584
585impl DependencyState {
586 pub fn new() -> Self {
588 Self {
589 graph: RwLock::new(DiGraph::new()),
590 resource_index: RwLock::new(HashMap::new()),
591 }
592 }
593
594 async fn ensure_node(&self, resource_id: &str, resource_type: ResourceNodeType) -> NodeIndex {
596 let mut index = self.resource_index.write().await;
597 let mut graph = self.graph.write().await;
598
599 if let Some(&node_idx) = index.get(resource_id) {
600 return node_idx;
601 }
602
603 let node = ResourceNode {
604 resource_id: resource_id.to_string(),
605 resource_type,
606 current_holder: None,
607 };
608
609 let node_idx = graph.add_node(node);
610 index.insert(resource_id.to_string(), node_idx);
611 node_idx
612 }
613
614 pub async fn add_dependency(&self, from: &str, to: &str, edge: DependencyEdge) {
620 let from_idx = self.ensure_node(from, ResourceNodeType::Generic).await;
621 let to_idx = self.ensure_node(to, ResourceNodeType::Generic).await;
622
623 self.graph.write().await.add_edge(to_idx, from_idx, edge);
626 }
627
628 pub async fn remove_dependency(&self, from: &str, to: &str) {
630 let index = self.resource_index.read().await;
631 let mut graph = self.graph.write().await;
632
633 if let (Some(&from_idx), Some(&to_idx)) = (index.get(from), index.get(to))
635 && let Some(edge) = graph.find_edge(to_idx, from_idx)
636 {
637 graph.remove_edge(edge);
638 }
639 }
640
641 pub async fn would_deadlock(&self, agent_id: &str, resources: &[String]) -> bool {
643 let mut graph = self.graph.write().await;
644 let mut index = self.resource_index.write().await;
645
646 let agent_node_id = format!("agent:{}", agent_id);
648
649 let agent_idx = if let Some(&idx) = index.get(&agent_node_id) {
651 idx
652 } else {
653 let node = ResourceNode {
654 resource_id: agent_node_id.clone(),
655 resource_type: ResourceNodeType::Agent(agent_id.to_string()),
656 current_holder: None,
657 };
658 let idx = graph.add_node(node);
659 index.insert(agent_node_id.clone(), idx);
660 idx
661 };
662
663 let mut temp_edges = Vec::new();
665 for resource in resources {
666 if let Some(&resource_idx) = index.get(resource) {
667 let edge = graph.add_edge(
668 agent_idx,
669 resource_idx,
670 DependencyEdge {
671 dependency_type: DependencyType::WaitsFor,
672 strength: DependencyStrength::Hard,
673 },
674 );
675 temp_edges.push(edge);
676 }
677 }
678
679 let has_cycle = is_cyclic_directed(&*graph);
681
682 for edge in temp_edges {
684 graph.remove_edge(edge);
685 }
686
687 has_cycle
688 }
689
690 pub async fn get_blocking_resources(&self, resource_id: &str) -> Vec<String> {
692 let graph = self.graph.read().await;
693 let index = self.resource_index.read().await;
694
695 let mut blocking = Vec::new();
696
697 if let Some(&node_idx) = index.get(resource_id) {
698 for edge_ref in graph.edges_directed(node_idx, Direction::Incoming) {
700 if let Some(source_node) = graph.node_weight(edge_ref.source())
701 && source_node.current_holder.is_some()
702 {
703 blocking.push(source_node.resource_id.clone());
704 }
705 }
706 }
707
708 blocking
709 }
710
711 pub async fn set_holder(&self, resource_id: &str, agent_id: Option<&str>) {
713 let index = self.resource_index.read().await;
714 let mut graph = self.graph.write().await;
715
716 if let Some(&node_idx) = index.get(resource_id)
717 && let Some(node) = graph.node_weight_mut(node_idx)
718 {
719 node.current_holder = agent_id.map(String::from);
720 }
721 }
722
723 pub async fn get_current_holders(&self) -> HashMap<String, String> {
725 let graph = self.graph.read().await;
726
727 graph
728 .node_weights()
729 .filter_map(|node| {
730 node.current_holder
731 .as_ref()
732 .map(|holder| (node.resource_id.clone(), holder.clone()))
733 })
734 .collect()
735 }
736
737 pub async fn get_agent_resources(&self, agent_id: &str) -> Vec<String> {
739 let graph = self.graph.read().await;
740
741 graph
742 .node_weights()
743 .filter_map(|node| {
744 if node.current_holder.as_deref() == Some(agent_id) {
745 Some(node.resource_id.clone())
746 } else {
747 None
748 }
749 })
750 .collect()
751 }
752
753 pub async fn get_execution_order(&self, operation_ids: &[String]) -> Vec<String> {
755 let graph = self.graph.read().await;
756 let index = self.resource_index.read().await;
757
758 let mut ordered = Vec::new();
761 let mut remaining: HashSet<_> = operation_ids.iter().cloned().collect();
762
763 while !remaining.is_empty() {
764 let mut made_progress = false;
765
766 for op_id in remaining.clone() {
767 if let Some(&node_idx) = index.get(&op_id) {
768 let all_deps_satisfied = graph
770 .edges_directed(node_idx, Direction::Incoming)
771 .all(|edge| {
772 graph
773 .node_weight(edge.source())
774 .map(|n| !remaining.contains(&n.resource_id))
775 .unwrap_or(true)
776 });
777
778 if all_deps_satisfied {
779 ordered.push(op_id.clone());
780 remaining.remove(&op_id);
781 made_progress = true;
782 }
783 } else {
784 ordered.push(op_id.clone());
786 remaining.remove(&op_id);
787 made_progress = true;
788 }
789 }
790
791 if !made_progress {
792 ordered.extend(remaining.drain());
795 break;
796 }
797 }
798
799 ordered
800 }
801}
802
803impl Default for DependencyState {
804 fn default() -> Self {
805 Self::new()
806 }
807}
808
809#[derive(Debug, Clone)]
811pub struct ResourceNode {
812 pub resource_id: String,
814 pub resource_type: ResourceNodeType,
816 pub current_holder: Option<String>,
818}
819
820#[derive(Debug, Clone)]
822pub enum ResourceNodeType {
823 File(PathBuf),
825 BuildLock,
827 TestLock,
829 GitIndex,
831 GitBranch(String),
833 Agent(String),
835 Generic,
837}
838
839#[derive(Debug, Clone)]
841pub struct DependencyEdge {
842 pub dependency_type: DependencyType,
844 pub strength: DependencyStrength,
846}
847
848#[derive(Debug, Clone, PartialEq, Eq)]
850pub enum DependencyType {
851 BlockedBy,
853 Produces,
855 ConflictsWith,
857 Reads,
859 Writes,
861 WaitsFor,
863}
864
865#[derive(Debug, Clone, PartialEq, Eq)]
867pub enum DependencyStrength {
868 Hard,
870 Soft,
872 Advisory,
874}
875
876#[derive(Debug, Clone)]
882pub struct ValidationResult {
883 pub valid: bool,
885 pub errors: Vec<String>,
887 pub warnings: Vec<String>,
889}
890
891impl ValidationResult {
892 pub fn ok() -> Self {
894 Self {
895 valid: true,
896 errors: Vec::new(),
897 warnings: Vec::new(),
898 }
899 }
900
901 pub fn error(msg: impl Into<String>) -> Self {
903 Self {
904 valid: false,
905 errors: vec![msg.into()],
906 warnings: Vec::new(),
907 }
908 }
909}
910
911#[derive(Debug, Clone)]
913pub struct StateModelProposedOperation {
914 pub agent_id: String,
916 pub operation_type: String,
918 pub resources_needed: Vec<String>,
920 pub resources_produced: Vec<String>,
922}
923
924#[derive(Debug, Clone)]
926pub struct StateChange {
927 pub operation_id: String,
929 pub application_changes: Vec<ApplicationChange>,
931 pub new_dependencies: Vec<(String, String, DependencyEdge)>,
933}
934
935#[derive(Debug, Clone)]
937pub enum ApplicationChange {
938 FileModified {
940 path: PathBuf,
942 new_hash: String,
944 },
945 ArtifactInvalidated {
947 artifact_id: String,
949 },
950 GitStateChanged {
952 new_state: GitState,
954 },
955 ResourceCreated {
957 resource_id: String,
959 },
960 ResourceDeleted {
962 resource_id: String,
964 },
965}
966
967#[derive(Debug, Clone)]
969pub struct StateSnapshot {
970 pub files: HashMap<PathBuf, FileStatus>,
972 pub locks: HashMap<String, String>,
974 pub git_state: GitState,
976 pub active_operations: Vec<String>,
978}
979
980#[cfg(test)]
981mod tests {
982 use super::*;
983
984 #[tokio::test]
985 async fn test_three_state_model_creation() {
986 let model = ThreeStateModel::new();
987 let snapshot = model.snapshot().await;
988
989 assert!(snapshot.files.is_empty());
990 assert!(snapshot.locks.is_empty());
991 assert!(snapshot.active_operations.is_empty());
992 }
993
994 #[tokio::test]
995 async fn test_application_state_file_tracking() {
996 let app_state = ApplicationState::new();
997
998 let path = PathBuf::from("/test/file.rs");
999 app_state
1000 .update_file(path.clone(), "hash123".to_string())
1001 .await;
1002
1003 let files = app_state.get_all_files().await;
1004 assert!(files.contains_key(&path));
1005 assert_eq!(files[&path].content_hash, "hash123");
1006 assert!(files[&path].dirty);
1007 }
1008
1009 #[tokio::test]
1010 async fn test_operation_state_lifecycle() {
1011 let op_state = OperationState::new();
1012
1013 let log = OperationLog::new(
1014 "op-1".to_string(),
1015 "agent-1".to_string(),
1016 "build".to_string(),
1017 serde_json::json!({}),
1018 );
1019
1020 let id = op_state.start_operation(log).await;
1021 assert_eq!(id, "op-1");
1022
1023 let active = op_state.get_active_operations().await;
1024 assert_eq!(active.len(), 1);
1025
1026 op_state.complete_operation(&id, true, None, None).await;
1027
1028 let active = op_state.get_active_operations().await;
1029 assert!(active.is_empty());
1030
1031 let op = op_state.get_operation(&id).await.unwrap();
1032 assert_eq!(op.status, OperationLogStatus::Completed);
1033 }
1034
1035 #[tokio::test]
1036 async fn test_dependency_state_deadlock_detection() {
1037 let dep_state = DependencyState::new();
1038
1039 dep_state
1041 .add_dependency(
1042 "resource-a",
1043 "resource-b",
1044 DependencyEdge {
1045 dependency_type: DependencyType::BlockedBy,
1046 strength: DependencyStrength::Hard,
1047 },
1048 )
1049 .await;
1050
1051 dep_state.set_holder("resource-a", Some("agent-1")).await;
1053
1054 let would_deadlock = dep_state
1056 .would_deadlock("agent-1", &["resource-b".to_string()])
1057 .await;
1058
1059 assert!(!would_deadlock);
1062 }
1063
1064 #[tokio::test]
1065 async fn test_validate_operation_conflict_detection() {
1066 let model = ThreeStateModel::new();
1067
1068 let log = OperationLog::new(
1070 "op-1".to_string(),
1071 "agent-1".to_string(),
1072 "build".to_string(),
1073 serde_json::json!({}),
1074 )
1075 .with_resources(vec!["resource-a".to_string()], vec![]);
1076
1077 model.operation_state.start_operation(log).await;
1078
1079 let proposed = StateModelProposedOperation {
1081 agent_id: "agent-2".to_string(),
1082 operation_type: "build".to_string(),
1083 resources_needed: vec!["resource-a".to_string()],
1084 resources_produced: vec![],
1085 };
1086
1087 let result = model.validate_operation(&proposed).await;
1088 assert!(!result.valid);
1089 assert!(!result.errors.is_empty());
1090 }
1091
1092 #[tokio::test]
1093 async fn test_state_change_recording() {
1094 let model = ThreeStateModel::new();
1095
1096 let change = StateChange {
1097 operation_id: "op-1".to_string(),
1098 application_changes: vec![
1099 ApplicationChange::FileModified {
1100 path: PathBuf::from("/test/file.rs"),
1101 new_hash: "newhash".to_string(),
1102 },
1103 ApplicationChange::ResourceCreated {
1104 resource_id: "build-artifact".to_string(),
1105 },
1106 ],
1107 new_dependencies: vec![],
1108 };
1109
1110 model.record_state_change(change).await;
1111
1112 let snapshot = model.snapshot().await;
1113 assert!(snapshot.files.contains_key(&PathBuf::from("/test/file.rs")));
1114 assert!(
1115 model
1116 .application_state
1117 .resource_exists("build-artifact")
1118 .await
1119 );
1120 }
1121
1122 #[tokio::test]
1123 async fn test_execution_order() {
1124 let dep_state = DependencyState::new();
1125
1126 dep_state
1128 .add_dependency(
1129 "op-a",
1130 "op-b",
1131 DependencyEdge {
1132 dependency_type: DependencyType::BlockedBy,
1133 strength: DependencyStrength::Hard,
1134 },
1135 )
1136 .await;
1137
1138 let order = dep_state
1139 .get_execution_order(&["op-a".to_string(), "op-b".to_string()])
1140 .await;
1141
1142 let pos_a = order.iter().position(|x| x == "op-a").unwrap();
1144 let pos_b = order.iter().position(|x| x == "op-b").unwrap();
1145 assert!(pos_b < pos_a);
1146 }
1147}