| 1 | //! FEAT-023 Phase 4/6 test support: a deterministic canned implementation of |
| 2 | //! `CommandSessionLifecycleContext` so portable handlers are unit-tested for |
| 3 | //! exact message/action composition without host state. |
| 4 | |
| 5 | use std::cell::Cell; |
| 6 | |
| 7 | use codewhale_command_contract::facets::{ |
| 8 | CommandSessionLifecycleContext, SessionArchiveReceipt, SessionBranchOutcome, |
| 9 | SessionForkFromReceipt, SessionForkReceipt, SessionNewReceipt, SessionSaveReceipt, |
| 10 | SessionSyncPayload, TreeBodyProjection, |
| 11 | }; |
| 12 | use std::path::PathBuf; |
| 13 | |
| 14 | /// Every delegate returns the canned value set by the test and records its |
| 15 | /// arguments so handler-routing assertions cannot pass without the expected |
| 16 | /// facet call. Unconfigured result slots return a descriptive canned error. |
| 17 | pub(crate) struct CannedLifecycle { |
| 18 | pub blocked: bool, |
| 19 | pub transition_checks: Cell<usize>, |
| 20 | pub leaf_hint: Option<String>, |
| 21 | pub branch: Result<SessionBranchOutcome, String>, |
| 22 | pub tree: Result<TreeBodyProjection, String>, |
| 23 | pub save: Result<SessionSaveReceipt, String>, |
| 24 | pub fork_active: Result<SessionForkReceipt, String>, |
| 25 | pub fork_from: Result<SessionForkFromReceipt, String>, |
| 26 | pub fresh: Result<SessionNewReceipt, String>, |
| 27 | pub load: Result<PathBuf, String>, |
| 28 | pub archived: Result<SessionArchiveReceipt, String>, |
| 29 | pub prune: Result<usize, String>, |
| 30 | pub branch_entries: Vec<String>, |
| 31 | pub save_paths: Vec<Option<String>>, |
| 32 | pub fork_sources: Vec<String>, |
| 33 | pub fresh_forces: Vec<bool>, |
| 34 | pub load_paths: Vec<String>, |
| 35 | pub picker_calls: Vec<Option<String>>, |
| 36 | pub archive_calls: Vec<(String, bool)>, |
| 37 | pub prune_days: Vec<u64>, |
| 38 | } |
| 39 | |
| 40 | impl Default for CannedLifecycle { |
| 41 | fn default() -> Self { |
| 42 | Self { |
| 43 | blocked: false, |
| 44 | transition_checks: Cell::new(0), |
| 45 | leaf_hint: None, |
| 46 | branch: Err("canned: branch_to not configured".to_string()), |
| 47 | tree: Ok(TreeBodyProjection::NoSession), |
| 48 | save: Err("canned: save not configured".to_string()), |
| 49 | fork_active: Err("canned: fork_active not configured".to_string()), |
| 50 | fork_from: Err("canned: fork_from not configured".to_string()), |
| 51 | fresh: Err("canned: fresh_session not configured".to_string()), |
| 52 | load: Err("canned: load not configured".to_string()), |
| 53 | archived: Err("canned: set_archived not configured".to_string()), |
| 54 | prune: Err("canned: prune not configured".to_string()), |
| 55 | branch_entries: Vec::new(), |
| 56 | save_paths: Vec::new(), |
| 57 | fork_sources: Vec::new(), |
| 58 | fresh_forces: Vec::new(), |
| 59 | load_paths: Vec::new(), |
| 60 | picker_calls: Vec::new(), |
| 61 | archive_calls: Vec::new(), |
| 62 | prune_days: Vec::new(), |
| 63 | } |
| 64 | } |
| 65 | } |
| 66 | |
| 67 | pub(crate) fn sync_payload(session_id: &str) -> SessionSyncPayload { |
| 68 | SessionSyncPayload { |
| 69 | session_id: Some(session_id.to_string()), |
| 70 | messages: vec![], |
| 71 | system_prompt: None, |
| 72 | model: "test-model".to_string(), |
| 73 | workspace: PathBuf::from("/workspace"), |
| 74 | mode: codewhale_command_contract::types::CommandMode::Agent, |
| 75 | } |
| 76 | } |
| 77 | |
| 78 | impl CommandSessionLifecycleContext for CannedLifecycle { |
| 79 | fn transition_blocked(&self) -> bool { |
| 80 | self.transition_checks |
| 81 | .set(self.transition_checks.get().saturating_add(1)); |
| 82 | self.blocked |
| 83 | } |
| 84 | fn branch_current_leaf_hint(&self) -> Option<String> { |
| 85 | self.leaf_hint.clone() |
| 86 | } |
| 87 | fn branch_to(&mut self, entry_id: &str) -> Result<SessionBranchOutcome, String> { |
| 88 | self.branch_entries.push(entry_id.to_string()); |
| 89 | self.branch.clone() |
| 90 | } |
| 91 | fn tree_body(&self) -> Result<TreeBodyProjection, String> { |
| 92 | self.tree.clone() |
| 93 | } |
| 94 | fn save_session(&mut self, path: Option<String>) -> Result<SessionSaveReceipt, String> { |
| 95 | self.save_paths.push(path); |
| 96 | self.save.clone() |
| 97 | } |
| 98 | fn fork_active(&mut self) -> Result<SessionForkReceipt, String> { |
| 99 | self.fork_active.clone() |
| 100 | } |
| 101 | fn fork_from(&mut self, id: &str) -> Result<SessionForkFromReceipt, String> { |
| 102 | self.fork_sources.push(id.to_string()); |
| 103 | self.fork_from.clone() |
| 104 | } |
| 105 | fn fresh_session(&mut self, force: bool) -> Result<SessionNewReceipt, String> { |
| 106 | self.fresh_forces.push(force); |
| 107 | self.fresh.clone() |
| 108 | } |
| 109 | fn load_session(&mut self, path: &str) -> Result<PathBuf, String> { |
| 110 | self.load_paths.push(path.to_string()); |
| 111 | self.load.clone() |
| 112 | } |
| 113 | fn open_picker(&mut self, preselected: Option<String>) { |
| 114 | self.picker_calls.push(preselected); |
| 115 | } |
| 116 | fn set_archived(&mut self, id: &str, archived: bool) -> Result<SessionArchiveReceipt, String> { |
| 117 | self.archive_calls.push((id.to_string(), archived)); |
| 118 | self.archived.clone() |
| 119 | } |
| 120 | fn prune_sessions(&mut self, days: u64) -> Result<usize, String> { |
| 121 | self.prune_days.push(days); |
| 122 | self.prune.clone() |
| 123 | } |
| 124 | } |
| 125 |