返回 CodeWhale
contextual_tips_pty.rs
根目录 / crates / tui / tests / cucumber / contextual_tips_pty.rs
1 //! User preference and impression receipts through the real terminal host.
2 //! Every process uses a sealed HOME; commands never submit a model request.
3
4 use std::time::Duration;
5
6 use super::qa_harness::{
7 harness::{Harness, SealedWorkspace, make_sealed_workspace},
8 keys,
9 };
10
11 const TIMEOUT: Duration = Duration::from_secs(10);
12
13 fn launch(workspace: &SealedWorkspace) -> Harness {
14 let mut tui = Harness::builder(Harness::cargo_bin("codewhale-tui"))
15 .cwd(workspace.workspace())
16 .clear_env()
17 .seal_home(workspace.home())
18 .env("CODEWHALE_DISABLE_MODELS_DEV_FETCH", "1")
19 .env("CODEWHALE_NO_UPDATE_CHECK", "1")
20 .env("CODEWHALE_TELEMETRY", "0")
21 .env("NO_ANIMATIONS", "1")
22 .args([
23 "--workspace",
24 workspace.workspace().to_str().unwrap(),
25 "--no-project-config",
26 "--fresh",
27 ])
28 .size(32, 100)
29 .spawn()
30 .unwrap();
31 tui.wait_for_text("Type a message", TIMEOUT).unwrap();
32 tui
33 }
34
35 fn command(tui: &mut Harness, input: &str, expected: &str) {
36 tui.paste(input).unwrap();
37 tui.wait_for_text(&format!("❯ {input}"), TIMEOUT).unwrap();
38 tui.send(keys::key::enter()).unwrap();
39 tui.wait_for_text("Type a message", TIMEOUT).unwrap();
40 tui.wait_for_text(expected, TIMEOUT).unwrap();
41 }
42
43 fn clear_draft(tui: &mut Harness) {
44 tui.paste("unsent draft").unwrap();
45 tui.wait_for_text("unsent draft", TIMEOUT).unwrap();
46 tui.send(keys::key::ctrl('u')).unwrap();
47 tui.wait_for_text("Type a message", TIMEOUT).unwrap();
48 tui.wait_for_idle(Duration::from_millis(150), TIMEOUT)
49 .unwrap();
50 }
51
52 fn settings(workspace: &SealedWorkspace) -> toml::Value {
53 let path = workspace.home().join(".codewhale/settings.toml");
54 toml::from_str(&std::fs::read_to_string(path).unwrap()).unwrap()
55 }
56
57 fn impressions(workspace: &SealedWorkspace) -> i64 {
58 settings(workspace)
59 .get("behavioral_tip_impressions")
60 .and_then(|counts| counts.get("cleared_input_restore"))
61 .and_then(toml::Value::as_integer)
62 .unwrap_or(0)
63 }
64
65 #[test]
66 fn contextual_tips_opt_out_survives_restart_and_preserves_caps() {
67 let workspace = make_sealed_workspace().unwrap();
68 let mut tui = launch(&workspace);
69 command(
70 &mut tui,
71 "/config contextual_tips off --save",
72 "contextual_tips = false",
73 );
74 assert_eq!(
75 settings(&workspace)["contextual_tips"].as_bool(),
76 Some(false)
77 );
78 clear_draft(&mut tui);
79 assert!(!tui.frame().contains("Cleared"));
80 assert_eq!(impressions(&workspace), 0);
81 tui.shutdown();
82
83 let mut tui = launch(&workspace);
84 command(
85 &mut tui,
86 "/config contextual_tips",
87 "contextual_tips = false",
88 );
89 clear_draft(&mut tui);
90 assert!(!tui.frame().contains("Cleared"));
91 assert_eq!(impressions(&workspace), 0);
92
93 // The existing Settings row supports pointer activation as well as the
94 // command route. One click selects; the second activates the same row.
95 tui.send(keys::key::f2()).unwrap();
96 tui.wait_for_text("Config", TIMEOUT).unwrap();
97 for ch in "tips".chars() {
98 tui.send(ch.to_string()).unwrap();
99 }
100 tui.wait_for_text("Search: tips", TIMEOUT).unwrap();
101 tui.wait_for_text("Contextual tips", TIMEOUT).unwrap();
102 let frame = tui.frame();
103 let row = (0..frame.rows())
104 .find(|&row| frame.row(row).contains("Contextual tips"))
105 .unwrap();
106 tui.send(keys::mouse::click(row, 4)).unwrap();
107 tui.wait_for_idle(Duration::from_millis(150), TIMEOUT)
108 .unwrap();
109 assert_eq!(
110 settings(&workspace)["contextual_tips"].as_bool(),
111 Some(false)
112 );
113 tui.send(keys::mouse::click(row, 4)).unwrap();
114 tui.wait_for(
115 |frame| {
116 (0..frame.rows()).any(|row| {
117 let text = frame.row(row);
118 text.contains("Contextual tips") && text.contains("On")
119 })
120 },
121 TIMEOUT,
122 )
123 .unwrap();
124 assert_eq!(
125 settings(&workspace)["contextual_tips"].as_bool(),
126 Some(true)
127 );
128 for _ in 0..2 {
129 tui.send(keys::key::esc()).unwrap();
130 tui.wait_for_idle(Duration::from_millis(150), TIMEOUT)
131 .unwrap();
132 }
133 clear_draft(&mut tui);
134 tui.wait_for_text("Cleared", TIMEOUT).unwrap();
135 assert_eq!(impressions(&workspace), 1);
136 clear_draft(&mut tui);
137 assert_eq!(impressions(&workspace), 1);
138 command(
139 &mut tui,
140 "/config contextual_tips off --save",
141 "contextual_tips = false",
142 );
143 command(
144 &mut tui,
145 "/config contextual_tips on --save",
146 "contextual_tips = true",
147 );
148 clear_draft(&mut tui);
149 assert_eq!(
150 impressions(&workspace),
151 1,
152 "toggle must not reset the session cap"
153 );
154 tui.shutdown();
155
156 for expected in [2, 2] {
157 let mut tui = launch(&workspace);
158 command(
159 &mut tui,
160 "/config contextual_tips",
161 "contextual_tips = true",
162 );
163 let before = impressions(&workspace);
164 clear_draft(&mut tui);
165 assert_eq!(impressions(&workspace), expected);
166 if before == 2 {
167 assert!(
168 !tui.frame().contains("Cleared"),
169 "lifetime cap still suppresses the tip"
170 );
171 }
172 tui.shutdown();
173 }
174 }
175
176 #[test]
177 fn contextual_tips_failed_save_is_visible_and_preserves_settings() {
178 let workspace = make_sealed_workspace().unwrap();
179 let mut tui = launch(&workspace);
180 command(
181 &mut tui,
182 "/config contextual_tips",
183 "contextual_tips = true",
184 );
185 let path = workspace.home().join(".codewhale/settings.toml");
186 let malformed = "contextual_tips = [private_fixture_payload\n";
187 std::fs::write(&path, malformed).unwrap();
188 command(
189 &mut tui,
190 "/config contextual_tips off --save",
191 "could not be saved",
192 );
193 assert!(!tui.frame().contains("private_fixture_payload"));
194 assert_eq!(std::fs::read_to_string(&path).unwrap(), malformed);
195 command(
196 &mut tui,
197 "/config contextual_tips",
198 "contextual_tips = false",
199 );
200 clear_draft(&mut tui);
201 assert!(!tui.frame().contains("Cleared"));
202 assert_eq!(std::fs::read_to_string(path).unwrap(), malformed);
203 tui.shutdown();
204 }
205
205 lines RUST