返回 CodeWhale
control_test_support.rs
根目录 / crates / tui / src / commands / groups / session / control_test_support.rs
1 //! FEAT-024 Phase 4: deterministic fake session-control facet for portable
2 //! handler tests. Canned returns plus a call log prove exact strings, actions,
3 //! call arguments, operation counts, and check order without touching the host.
4
5 #![cfg(test)]
6
7 use codewhale_command_contract::facets::{
8 CommandSessionControlContext, HostedWorkTarget, PlanProjection, RelayProjection, RemoteLink,
9 RemoteOpenOutcome, RemoteStartInfo, ResumeImportReceipt, ResumeSource, SessionTitleReceipt,
10 TitleReport, TodoProjection,
11 };
12 use std::cell::RefCell;
13 use std::path::PathBuf;
14
15 #[derive(Default)]
16 pub(crate) struct FakeControl {
17 pub(crate) blocked: bool,
18 pub(crate) relay: Option<RelayProjection>,
19 pub(crate) resume: Option<Result<ResumeSource, String>>,
20 pub(crate) import: Option<Result<ResumeImportReceipt, String>>,
21 pub(crate) sanitized_title: Option<String>,
22 pub(crate) rename: Option<Result<SessionTitleReceipt, String>>,
23 pub(crate) title_report: Option<TitleReport>,
24 pub(crate) set_title: Option<Result<(), String>>,
25 pub(crate) clear_title: Option<Result<(), String>>,
26 pub(crate) remote_status: Option<String>,
27 pub(crate) remote_link: Option<Option<RemoteLink>>,
28 pub(crate) browser_open: Option<RemoteOpenOutcome>,
29 pub(crate) start_info: Option<RemoteStartInfo>,
30 pub(crate) stop_refusal: Option<Option<String>>,
31 pub(crate) hosted: Option<Option<HostedWorkTarget>>,
32 pub(crate) calls: RefCell<Vec<String>>,
33 }
34
35 pub(crate) fn message(result: &super::CommandResult) -> &str {
36 result
37 .message
38 .as_deref()
39 .map(|message| message.strip_prefix("Error: ").unwrap_or(message))
40 .unwrap_or("")
41 }
42
43 impl FakeControl {
44 fn call(&self, name: &str, arg: Option<&str>) {
45 let mut calls = self.calls.borrow_mut();
46 match arg {
47 Some(arg) => calls.push(format!("{name}({arg})")),
48 None => calls.push(name.to_string()),
49 }
50 }
51 }
52
53 impl CommandSessionControlContext for FakeControl {
54 fn transition_blocked(&self) -> bool {
55 self.call("transition_blocked", None);
56 self.blocked
57 }
58 fn relay_projection(&self) -> RelayProjection {
59 self.call("relay_projection", None);
60 self.relay.clone().expect("unexpected relay_projection()")
61 }
62 fn open_resume_picker(&mut self) {
63 self.call("open_resume_picker", None);
64 }
65 fn resolve_resume_source(&mut self, raw: &str) -> Result<ResumeSource, String> {
66 self.call("resolve_resume_source", Some(raw));
67 match self.resume.clone() {
68 Some(result) => result,
69 None => Ok(ResumeSource::NotFound {
70 raw: raw.to_string(),
71 error: "missing".to_string(),
72 }),
73 }
74 }
75 fn import_session_file(&mut self, path: PathBuf) -> Result<ResumeImportReceipt, String> {
76 self.call("import_session_file", Some(&path.display().to_string()));
77 match self.import.clone() {
78 Some(result) => result,
79 None => Err(format!(
80 "unexpected import_session_file({}) on empty fake",
81 path.display()
82 )),
83 }
84 }
85 fn sanitize_session_title(&self, raw: &str) -> String {
86 self.call("sanitize_session_title", Some(raw));
87 self.sanitized_title
88 .clone()
89 .unwrap_or_else(|| raw.to_string())
90 }
91 fn rename_session(&mut self, title: &str) -> Result<SessionTitleReceipt, String> {
92 self.call("rename_session", Some(title));
93 match self.rename.clone() {
94 Some(result) => result,
95 None => Err(format!("unexpected rename_session({title}) on empty fake")),
96 }
97 }
98 fn title_report(&self) -> TitleReport {
99 self.call("title_report", None);
100 self.title_report
101 .clone()
102 .expect("unexpected title_report()")
103 }
104 fn set_window_title(&mut self, title: String) -> Result<(), String> {
105 self.call("set_window_title", Some(&title));
106 match self.set_title.clone() {
107 Some(result) => result,
108 None => Err("unexpected set_window_title on empty fake".to_string()),
109 }
110 }
111 fn clear_window_title(&mut self) -> Result<(), String> {
112 self.call("clear_window_title", None);
113 match self.clear_title.clone() {
114 Some(result) => result,
115 None => Err("unexpected clear_window_title on empty fake".to_string()),
116 }
117 }
118 fn remote_status(&self) -> String {
119 self.call("remote_status", None);
120 self.remote_status
121 .clone()
122 .expect("unexpected remote_status()")
123 }
124 fn remote_link(&self) -> Option<RemoteLink> {
125 self.call("remote_link", None);
126 self.remote_link.clone().expect("unexpected remote_link()")
127 }
128 fn remote_browser_open(&self) -> RemoteOpenOutcome {
129 self.call("remote_browser_open", None);
130 self.browser_open
131 .clone()
132 .expect("unexpected browser_open()")
133 }
134 fn remote_start_info(&self) -> RemoteStartInfo {
135 self.call("remote_start_info", None);
136 self.start_info.clone().expect("unexpected start_info()")
137 }
138 fn remote_stop_refusal(&self) -> Option<String> {
139 self.call("remote_stop_refusal", None);
140 self.stop_refusal
141 .clone()
142 .expect("unexpected stop_refusal()")
143 }
144 fn resolve_hosted_work_target(&self) -> Option<HostedWorkTarget> {
145 self.call("resolve_hosted_work_target", None);
146 self.hosted.clone().expect("unexpected hosted()")
147 }
148 }
149
150 pub(crate) fn relay_projection_fixture() -> RelayProjection {
151 RelayProjection {
152 compact_template: "# Session relay".to_string(),
153 workspace: "/work".to_string(),
154 mode: "operate".to_string(),
155 model: "model-x".to_string(),
156 goal_objective: Some("objective-y".to_string()),
157 goal_token_budget: Some(900),
158 todos: TodoProjection::Absent,
159 plan: PlanProjection::Absent,
160 }
161 }
162
162 lines RUST