返回 CodeWhale
palette_audit.rs
根目录 / crates / tui / tests / integration / palette_audit.rs
1 //! Palette audit tests to prevent color drift.
2 //!
3 //! These tests ensure that deprecated colors are not used directly in
4 //! user-visible code. Backward-compatible DeepSeek aliases should point
5 //! at the current Codewhale semantic tokens instead of stale brand RGBs.
6
7 use ratatui::style::Color;
8
9 fn color_to_rgb(color: Color) -> (u8, u8, u8) {
10 match color {
11 Color::Rgb(r, g, b) => (r, g, b),
12 Color::Black => (0, 0, 0),
13 Color::White => (255, 255, 255),
14 Color::Gray => (128, 128, 128),
15 Color::DarkGray => (169, 169, 169),
16 Color::Red => (255, 0, 0),
17 Color::LightRed => (255, 102, 102),
18 Color::Green => (0, 255, 0),
19 Color::LightGreen => (102, 255, 102),
20 Color::Yellow => (255, 255, 0),
21 Color::LightYellow => (255, 255, 153),
22 Color::Blue => (0, 0, 255),
23 Color::LightBlue => (102, 153, 255),
24 Color::Magenta => (255, 0, 255),
25 Color::LightMagenta => (255, 153, 255),
26 Color::Cyan => (0, 255, 255),
27 Color::LightCyan => (153, 255, 255),
28 _ => panic!("unsupported color variant for contrast test: {color:?}"),
29 }
30 }
31
32 fn linearize_srgb(component: u8) -> f64 {
33 let srgb = f64::from(component) / 255.0;
34 if srgb <= 0.04045 {
35 srgb / 12.92
36 } else {
37 ((srgb + 0.055) / 1.055).powf(2.4)
38 }
39 }
40
41 fn relative_luminance(color: Color) -> f64 {
42 let (r, g, b) = color_to_rgb(color);
43 0.2126 * linearize_srgb(r) + 0.7152 * linearize_srgb(g) + 0.0722 * linearize_srgb(b)
44 }
45
46 fn contrast_ratio(foreground: Color, background: Color) -> f64 {
47 let fg = relative_luminance(foreground);
48 let bg = relative_luminance(background);
49 if fg >= bg {
50 (fg + 0.05) / (bg + 0.05)
51 } else {
52 (bg + 0.05) / (fg + 0.05)
53 }
54 }
55
56 fn assert_min_contrast(label: &str, foreground: Color, background: Color, min_ratio: f64) {
57 let ratio = contrast_ratio(foreground, background);
58 assert!(
59 ratio >= min_ratio,
60 "{label} contrast {ratio:.2} is below minimum {min_ratio:.2}"
61 );
62 }
63
64 // NOTE: The deprecated color audit (DEEPSEEK_AQUA) was removed because
65 // the deprecated constant no longer exists in the palette.
66
67 #[test]
68 fn verify_status_success_uses_success_token() {
69 assert_eq!(
70 codewhale_palette::STATUS_SUCCESS,
71 Color::Rgb(
72 codewhale_palette::WHALE_SUCCESS_RGB.0,
73 codewhale_palette::WHALE_SUCCESS_RGB.1,
74 codewhale_palette::WHALE_SUCCESS_RGB.2
75 ),
76 "STATUS_SUCCESS should use the current success token"
77 );
78 assert_ne!(
79 codewhale_palette::STATUS_SUCCESS,
80 codewhale_palette::WHALE_ACTION,
81 "STATUS_SUCCESS should not regress to the primary accent"
82 );
83 }
84
85 #[test]
86 fn whale_roles_are_pinned_and_non_colliding() {
87 assert_eq!(codewhale_palette::WHALE_BG_RGB, (7, 12, 29));
88 assert_eq!(codewhale_palette::WHALE_PANEL_RGB, (16, 28, 64));
89 assert_eq!(codewhale_palette::WHALE_ELEVATED_RGB, (26, 44, 99));
90 assert_eq!(codewhale_palette::WHALE_ACTION_RGB, (106, 166, 220));
91 assert_eq!(
92 codewhale_palette::WHALE_ACCENT_SECONDARY_RGB,
93 (79, 209, 197)
94 );
95 assert_eq!(codewhale_palette::WHALE_HUMAN_RGB, (246, 196, 83));
96 assert_eq!(codewhale_palette::WHALE_WARNING_RGB, (255, 122, 89));
97 assert_eq!(codewhale_palette::WHALE_ERROR_RGB, (255, 134, 178));
98 assert_eq!(codewhale_palette::WHALE_MODE_AGENT_RGB, (126, 180, 232));
99 assert_eq!(codewhale_palette::WHALE_MODE_YOLO_RGB, (255, 112, 160));
100 assert_eq!(codewhale_palette::WHALE_MODE_PLAN_RGB, (185, 220, 236));
101 assert_eq!(codewhale_palette::WHALE_MODE_OPERATE_RGB, (173, 136, 255));
102 assert_eq!(codewhale_palette::LIGHT_SUCCESS_FG_RGB, (20, 118, 61));
103 assert_eq!(codewhale_palette::LIGHT_MODE_AGENT_RGB, (22, 54, 178));
104 assert_eq!(codewhale_palette::LIGHT_MODE_PLAN_RGB, (52, 92, 128));
105 assert_eq!(codewhale_palette::LIGHT_OPERATE_RGB, (112, 71, 184));
106 assert_eq!(codewhale_palette::LIGHT_MODE_YOLO_RGB, (181, 35, 90));
107 assert_eq!(
108 codewhale_palette::LIGHT_USER_BODY,
109 codewhale_palette::LIGHT_SUCCESS_FG
110 );
111
112 let ui = codewhale_palette::UI_THEME;
113 assert_eq!(ui.accent_primary, codewhale_palette::WHALE_ACTION);
114 assert_eq!(ui.info, codewhale_palette::WHALE_ACTION);
115 assert_eq!(ui.status_working, codewhale_palette::WHALE_LIVE);
116 assert_eq!(ui.accent_action, codewhale_palette::WHALE_HUMAN);
117 assert_eq!(ui.warning, codewhale_palette::STATUS_WARNING);
118 assert_eq!(ui.error_fg, codewhale_palette::WHALE_ERROR);
119 assert_eq!(ui.mode_operate, codewhale_palette::MODE_OPERATE);
120 assert_ne!(
121 ui.mode_plan, ui.accent_action,
122 "Plan is structural; Signal Gold is reserved for human attention"
123 );
124 assert_ne!(
125 ui.status_working, ui.success,
126 "live and done need separate ink"
127 );
128 assert_ne!(ui.accent_action, ui.warning, "human asks are not warnings");
129 assert_ne!(
130 ui.warning, ui.error_fg,
131 "warning and danger must not collapse"
132 );
133
134 let foreground_domains = [
135 ("action", codewhale_palette::WHALE_ACTION),
136 ("live", codewhale_palette::WHALE_LIVE),
137 ("human", codewhale_palette::WHALE_HUMAN),
138 ("success", codewhale_palette::STATUS_SUCCESS),
139 ("warning", codewhale_palette::STATUS_WARNING),
140 ("danger", codewhale_palette::WHALE_ERROR),
141 ("agent mode", codewhale_palette::MODE_AGENT),
142 ("full-access mode", codewhale_palette::MODE_YOLO),
143 ("plan mode", codewhale_palette::MODE_PLAN),
144 ("operate mode", codewhale_palette::MODE_OPERATE),
145 ("reasoning", codewhale_palette::TEXT_REASONING),
146 ("diff added", codewhale_palette::DIFF_ADDED),
147 ];
148 for (index, (left_name, left)) in foreground_domains.iter().enumerate() {
149 for (right_name, right) in foreground_domains.iter().skip(index + 1) {
150 assert_ne!(
151 left, right,
152 "raw foreground adaptation domains '{left_name}' and '{right_name}' collide"
153 );
154 }
155 }
156
157 let background_domains = [
158 ("base", codewhale_palette::WHALE_BG),
159 ("panel", codewhale_palette::WHALE_PANEL),
160 ("composer", codewhale_palette::WHALE_COMPOSER),
161 ("elevated", codewhale_palette::SURFACE_ELEVATED),
162 ("tool", codewhale_palette::SURFACE_TOOL),
163 ("tool active", codewhale_palette::SURFACE_TOOL_ACTIVE),
164 ("reasoning", codewhale_palette::SURFACE_REASONING),
165 ("reasoning tint", codewhale_palette::SURFACE_REASONING_TINT),
166 (
167 "reasoning active",
168 codewhale_palette::SURFACE_REASONING_ACTIVE,
169 ),
170 ("success", codewhale_palette::SURFACE_SUCCESS),
171 ("error", codewhale_palette::SURFACE_ERROR),
172 ("selection", codewhale_palette::SELECTION_BG),
173 ("diff added", codewhale_palette::DIFF_ADDED_BG),
174 ("diff deleted", codewhale_palette::DIFF_DELETED_BG),
175 ];
176 for (index, (left_name, left)) in background_domains.iter().enumerate() {
177 for (right_name, right) in background_domains.iter().skip(index + 1) {
178 assert_ne!(
179 left, right,
180 "raw background adaptation domains '{left_name}' and '{right_name}' collide"
181 );
182 }
183 }
184 }
185
186 #[test]
187 fn contrast_guardrails_for_key_ui_pairs() {
188 let min_readable = 4.5;
189
190 assert_min_contrast(
191 "TEXT_BODY on WHALE_BG",
192 codewhale_palette::TEXT_BODY,
193 codewhale_palette::WHALE_BG,
194 min_readable,
195 );
196 assert_min_contrast(
197 "TEXT_SECONDARY on WHALE_BG",
198 codewhale_palette::TEXT_SECONDARY,
199 codewhale_palette::WHALE_BG,
200 min_readable,
201 );
202 assert_min_contrast(
203 "TEXT_HINT on WHALE_BG",
204 codewhale_palette::TEXT_HINT,
205 codewhale_palette::WHALE_BG,
206 min_readable,
207 );
208 assert_min_contrast(
209 "STATUS_WARNING on WHALE_BG",
210 codewhale_palette::STATUS_WARNING,
211 codewhale_palette::WHALE_BG,
212 min_readable,
213 );
214 assert_min_contrast(
215 "STATUS_ERROR on WHALE_BG",
216 codewhale_palette::STATUS_ERROR,
217 codewhale_palette::WHALE_BG,
218 min_readable,
219 );
220 assert_min_contrast(
221 "SELECTION_TEXT on SELECTION_BG",
222 codewhale_palette::SELECTION_TEXT,
223 codewhale_palette::SELECTION_BG,
224 min_readable,
225 );
226 assert_min_contrast(
227 "TEXT_PRIMARY on SURFACE_ELEVATED",
228 codewhale_palette::TEXT_PRIMARY,
229 codewhale_palette::SURFACE_ELEVATED,
230 min_readable,
231 );
232 for (label, foreground) in [
233 ("action", codewhale_palette::UI_THEME.accent_primary),
234 ("live", codewhale_palette::UI_THEME.status_working),
235 ("human", codewhale_palette::UI_THEME.accent_action),
236 ("warning", codewhale_palette::UI_THEME.warning),
237 ("danger", codewhale_palette::UI_THEME.error_fg),
238 ("act mode", codewhale_palette::UI_THEME.mode_agent),
239 ("plan mode", codewhale_palette::UI_THEME.mode_plan),
240 ("operate", codewhale_palette::UI_THEME.mode_operate),
241 ("full-access mode", codewhale_palette::UI_THEME.mode_yolo),
242 ("success", codewhale_palette::UI_THEME.success),
243 ] {
244 assert_min_contrast(
245 label,
246 foreground,
247 codewhale_palette::SURFACE_ELEVATED,
248 min_readable,
249 );
250 }
251 let light_foregrounds = [
252 ("body", codewhale_palette::LIGHT_UI_THEME.text_body),
253 ("soft", codewhale_palette::LIGHT_UI_THEME.text_soft),
254 ("muted", codewhale_palette::LIGHT_UI_THEME.text_muted),
255 ("hint", codewhale_palette::LIGHT_UI_THEME.text_hint),
256 ("action", codewhale_palette::LIGHT_UI_THEME.accent_primary),
257 ("live", codewhale_palette::LIGHT_UI_THEME.status_working),
258 ("human", codewhale_palette::LIGHT_UI_THEME.accent_action),
259 ("warning", codewhale_palette::LIGHT_UI_THEME.warning),
260 ("danger", codewhale_palette::LIGHT_UI_THEME.error_fg),
261 ("act mode", codewhale_palette::LIGHT_UI_THEME.mode_agent),
262 ("plan mode", codewhale_palette::LIGHT_UI_THEME.mode_plan),
263 ("operate", codewhale_palette::LIGHT_UI_THEME.mode_operate),
264 (
265 "full-access mode",
266 codewhale_palette::LIGHT_UI_THEME.mode_yolo,
267 ),
268 ("success", codewhale_palette::LIGHT_UI_THEME.success),
269 ("user", codewhale_palette::LIGHT_USER_BODY),
270 ];
271 for (background_name, background) in [
272 ("surface", codewhale_palette::LIGHT_SURFACE),
273 ("panel", codewhale_palette::LIGHT_PANEL),
274 ("raised", codewhale_palette::LIGHT_ELEVATED),
275 ("selection", codewhale_palette::LIGHT_SELECTION_BG),
276 ("reasoning", codewhale_palette::LIGHT_REASONING),
277 ("success tint", codewhale_palette::LIGHT_SUCCESS),
278 ("error tint", codewhale_palette::LIGHT_ERROR),
279 ] {
280 for (foreground_name, foreground) in light_foregrounds {
281 assert_min_contrast(
282 &format!("light {foreground_name} on {background_name}"),
283 foreground,
284 background,
285 min_readable,
286 );
287 }
288 }
289 assert_min_contrast(
290 "light user row on raised",
291 codewhale_palette::LIGHT_USER_BODY,
292 codewhale_palette::LIGHT_ELEVATED,
293 min_readable,
294 );
295 assert_min_contrast(
296 "light work-surface success hover on raised",
297 codewhale_palette::LIGHT_UI_THEME.success,
298 codewhale_palette::LIGHT_UI_THEME.elevated_bg,
299 min_readable,
300 );
301 }
302
302 lines RUST