返回 CodeWhale
lifecycle_test_support.rs
根目录 / crates / tui / src / commands / groups / session / lifecycle_test_support.rs
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
125 lines RUST