leviso_cheat_test/lib.rs
1//! # leviso-cheat-test
2//!
3//! A proc-macro crate for creating "cheat-aware" tests that document how they
4//! could be cheated and what the consequences would be for users.
5//!
6//! # STOP. READ. THEN ACT.
7//!
8//! Before modifying this crate, read the existing implementation to understand
9//! how cheat documentation is structured.
10//!
11//! ## Why This Exists
12//!
13//! On 2026-01-20, a developer created false positives by moving missing binaries
14//! to "OPTIONAL" lists to make tests pass while shipping a broken product. This
15//! crate ensures that every test documents:
16//!
17//! 1. What user scenario it protects
18//! 2. How the test could be cheated
19//! 3. What users experience when the test is cheated
20//! 4. The severity and ease of cheating
21//!
22//! ## Usage
23//!
24//! ```rust,ignore
25//! use leviso_cheat_test::cheat_aware;
26//!
27//! #[cheat_aware(
28//! protects = "User can run sudo commands",
29//! severity = "CRITICAL",
30//! ease = "EASY",
31//! cheats = ["Move sudo to OPTIONAL list", "Remove sudo from essential binaries"],
32//! consequence = "bash: sudo: command not found",
33//! legitimate_change = "If sudo is genuinely not needed for a headless profile, \
34//! add it to the profile's optional list in builder/src/profiles.rs"
35//! )]
36//! #[test]
37//! fn test_sudo_binary_present() {
38//! assert!(tarball_contains("./usr/bin/sudo"));
39//! }
40//! ```
41//!
42//! ## On Failure
43//!
44//! When a cheat-aware test fails, it prints:
45//!
46//! ```text
47//! ======================================================================
48//! === TEST FAILED: test_sudo_binary_present ===
49//! ======================================================================
50//!
51//! PROTECTS: User can run sudo commands
52//! SEVERITY: CRITICAL
53//! EASE OF CHEATING: EASY
54//!
55//! CHEAT VECTORS:
56//! 1. Move sudo to OPTIONAL list
57//! 2. Remove sudo from essential binaries
58//!
59//! LEGITIMATE CHANGE PATH:
60//! If sudo is genuinely not needed for a headless profile,
61//! add it to the profile's optional list in builder/src/profiles.rs
62//!
63//! USER CONSEQUENCE:
64//! bash: sudo: command not found
65//!
66//! ORIGINAL ERROR:
67//! assertion failed: tarball_contains("./usr/bin/sudo")
68//! ======================================================================
69//! ```
70
71use proc_macro::TokenStream;
72use quote::quote;
73use syn::{
74 parse::{Parse, ParseStream},
75 parse_macro_input,
76 punctuated::Punctuated,
77 ExprLit, Ident, ItemFn, Lit, Token,
78};
79
80/// Metadata for a cheat-aware test.
81struct CheatAwareArgs {
82 protects: String,
83 severity: String,
84 ease: String,
85 cheats: Vec<String>,
86 consequence: String,
87 /// Optional: describes how to legitimately change behavior instead of cheating
88 legitimate_change: Option<String>,
89}
90
91impl Default for CheatAwareArgs {
92 fn default() -> Self {
93 Self {
94 protects: "UNSPECIFIED".to_string(),
95 severity: "UNSPECIFIED".to_string(),
96 ease: "UNSPECIFIED".to_string(),
97 cheats: vec!["UNSPECIFIED".to_string()],
98 consequence: "UNSPECIFIED".to_string(),
99 legitimate_change: None,
100 }
101 }
102}
103
104/// A single key = value or key = [value, ...] assignment.
105struct MetaItem {
106 key: Ident,
107 value: MetaValue,
108}
109
110enum MetaValue {
111 Str(String),
112 Array(Vec<String>),
113}
114
115impl Parse for MetaItem {
116 fn parse(input: ParseStream) -> syn::Result<Self> {
117 let key: Ident = input.parse()?;
118 let _: Token![=] = input.parse()?;
119
120 let value = if input.peek(syn::token::Bracket) {
121 // Parse array: [...]
122 let content;
123 syn::bracketed!(content in input);
124 let items: Punctuated<ExprLit, Token![,]> =
125 content.parse_terminated(ExprLit::parse, Token![,])?;
126
127 let strings: Vec<String> = items
128 .into_iter()
129 .filter_map(|expr| {
130 if let Lit::Str(s) = expr.lit {
131 Some(s.value())
132 } else {
133 None
134 }
135 })
136 .collect();
137
138 MetaValue::Array(strings)
139 } else {
140 // Parse string literal
141 let lit: ExprLit = input.parse()?;
142 if let Lit::Str(s) = lit.lit {
143 MetaValue::Str(s.value())
144 } else {
145 return Err(syn::Error::new_spanned(lit, "expected string literal"));
146 }
147 };
148
149 Ok(MetaItem { key, value })
150 }
151}
152
153impl Parse for CheatAwareArgs {
154 fn parse(input: ParseStream) -> syn::Result<Self> {
155 let mut args = CheatAwareArgs::default();
156
157 let items: Punctuated<MetaItem, Token![,]> =
158 input.parse_terminated(MetaItem::parse, Token![,])?;
159
160 for item in items {
161 let key = item.key.to_string();
162 match (key.as_str(), item.value) {
163 ("protects", MetaValue::Str(s)) => args.protects = s,
164 ("severity", MetaValue::Str(s)) => args.severity = s,
165 ("ease", MetaValue::Str(s)) => args.ease = s,
166 ("consequence", MetaValue::Str(s)) => args.consequence = s,
167 ("cheats", MetaValue::Array(arr)) => args.cheats = arr,
168 ("legitimate_change", MetaValue::Str(s)) => args.legitimate_change = Some(s),
169 (key, _) => {
170 return Err(syn::Error::new_spanned(
171 item.key,
172 format!(
173 "unknown key '{}'. Valid keys: protects, severity, ease, cheats, consequence, legitimate_change",
174 key
175 ),
176 ))
177 }
178 }
179 }
180
181 Ok(args)
182 }
183}
184
185/// Mark a test as cheat-aware, documenting how it could be cheated.
186///
187/// # Attributes
188///
189/// - `protects` - What user scenario this test protects (string)
190/// - `severity` - Impact severity: "CRITICAL", "HIGH", "MEDIUM", "LOW" (string)
191/// - `ease` - How easy it is to cheat: "EASY", "MEDIUM", "HARD" (string)
192/// - `cheats` - List of ways to cheat this test (array of strings)
193/// - `consequence` - What users see when cheated (string)
194/// - `legitimate_change` - Optional: how to legitimately change behavior (string)
195///
196/// The `legitimate_change` field implements "inoculation prompting" from Anthropic's
197/// research on emergent misalignment. By explicitly describing the legitimate path
198/// for changing behavior, we reduce the temptation to cheat.
199///
200/// # Example
201///
202/// ```rust,ignore
203/// #[cheat_aware(
204/// protects = "User can log in",
205/// severity = "CRITICAL",
206/// ease = "EASY",
207/// cheats = ["Skip PAM config check", "Accept any password"],
208/// consequence = "Authentication failure",
209/// legitimate_change = "If PAM is genuinely not needed (e.g., embedded system), \
210/// configure the profile in builder/src/profiles.rs with auth_method = None"
211/// )]
212/// #[test]
213/// fn test_login_works() {
214/// // ...
215/// }
216/// ```
217#[proc_macro_attribute]
218pub fn cheat_aware(args: TokenStream, input: TokenStream) -> TokenStream {
219 let args = parse_macro_input!(args as CheatAwareArgs);
220 let input_fn = parse_macro_input!(input as ItemFn);
221
222 let fn_name = &input_fn.sig.ident;
223 let fn_name_str = fn_name.to_string();
224 let fn_attrs = &input_fn.attrs;
225 let fn_vis = &input_fn.vis;
226 let fn_block = &input_fn.block;
227 let fn_asyncness = &input_fn.sig.asyncness;
228
229 let protects = &args.protects;
230 let severity = &args.severity;
231 let ease = &args.ease;
232 let consequence = &args.consequence;
233 let cheats = &args.cheats;
234 let legitimate_change = args.legitimate_change.as_deref().unwrap_or("");
235 let has_legitimate_change = args.legitimate_change.is_some();
236
237 // Build the cheat list as numbered items
238 let cheats_display: Vec<String> = cheats
239 .iter()
240 .enumerate()
241 .map(|(i, c)| format!(" {}. {}", i + 1, c))
242 .collect();
243 let cheats_joined = cheats_display.join("\n");
244
245 // For async functions, we can't use catch_unwind, so just run the body directly
246 // The test framework will catch any panics
247 let body = if fn_asyncness.is_some() {
248 quote! {
249 // Context variables (for documentation/debugging)
250 let _test_name = #fn_name_str;
251 let _protects = #protects;
252 let _severity = #severity;
253 let _ease = #ease;
254 let _consequence = #consequence;
255 let _cheats = #cheats_joined;
256 let _legitimate_change = #legitimate_change;
257
258 // Print what this test ensures (visible with --nocapture)
259 println!(" ensures: {}", _protects);
260
261 // For async tests, we run directly and let the test framework handle panics
262 // The cheat metadata is available in the source code for documentation
263 #fn_block
264 }
265 } else {
266 quote! {
267 // Context printed at test start (visible with --nocapture or on failure)
268 let _test_name = #fn_name_str;
269 let _protects = #protects;
270 let _severity = #severity;
271 let _ease = #ease;
272 let _consequence = #consequence;
273 let _cheats = #cheats_joined;
274 let _legitimate_change = #legitimate_change;
275 let _has_legitimate_change = #has_legitimate_change;
276
277 // Print what this test ensures (visible with --nocapture)
278 println!(" ensures: {}", _protects);
279
280 // Wrap the test body to enhance panic messages
281 let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
282 #fn_block
283 }));
284
285 if let Err(e) = result {
286 // Extract the panic message
287 let panic_msg = if let Some(s) = e.downcast_ref::<&str>() {
288 (*s).to_string()
289 } else if let Some(s) = e.downcast_ref::<String>() {
290 s.clone()
291 } else {
292 "Unknown panic".to_string()
293 };
294
295 // Print the enhanced failure message
296 eprintln!("\n{}", "=".repeat(70));
297 eprintln!("=== TEST FAILED: {} ===", _test_name);
298 eprintln!("{}", "=".repeat(70));
299 eprintln!();
300 eprintln!("PROTECTS: {}", _protects);
301 eprintln!("SEVERITY: {}", _severity);
302 eprintln!("EASE OF CHEATING: {}", _ease);
303 eprintln!();
304 eprintln!("CHEAT VECTORS:");
305 eprintln!("{}", _cheats);
306 if _has_legitimate_change {
307 eprintln!();
308 eprintln!("LEGITIMATE CHANGE PATH:");
309 eprintln!(" {}", _legitimate_change);
310 }
311 eprintln!();
312 eprintln!("USER CONSEQUENCE:");
313 eprintln!(" {}", _consequence);
314 eprintln!();
315 eprintln!("ORIGINAL ERROR:");
316 eprintln!(" {}", panic_msg);
317 eprintln!("{}", "=".repeat(70));
318 eprintln!();
319
320 // Re-panic to fail the test
321 std::panic::resume_unwind(e);
322 }
323 }
324 };
325
326 let expanded = quote! {
327 #(#fn_attrs)*
328 #fn_vis #fn_asyncness fn #fn_name() {
329 #body
330 }
331 };
332
333 TokenStream::from(expanded)
334}
335
336/// A simpler version of `cheat_aware` for tests where full metadata isn't needed.
337///
338/// Just marks a test as having been reviewed for cheat vectors.
339///
340/// # Example
341///
342/// ```rust,ignore
343/// #[cheat_reviewed("Unit test for version parsing - no cheat vectors")]
344/// #[test]
345/// fn test_parse_version() {
346/// // ...
347/// }
348/// ```
349#[proc_macro_attribute]
350pub fn cheat_reviewed(args: TokenStream, input: TokenStream) -> TokenStream {
351 let _reason: syn::LitStr = parse_macro_input!(args as syn::LitStr);
352 let input_fn = parse_macro_input!(input as ItemFn);
353
354 let fn_name = &input_fn.sig.ident;
355 let fn_attrs = &input_fn.attrs;
356 let fn_vis = &input_fn.vis;
357 let fn_block = &input_fn.block;
358 let fn_asyncness = &input_fn.sig.asyncness;
359
360 let expanded = quote! {
361 #(#fn_attrs)*
362 #fn_vis #fn_asyncness fn #fn_name() {
363 // This test has been reviewed for cheat vectors.
364 #fn_block
365 }
366 };
367
368 TokenStream::from(expanded)
369}
370
371/// Metadata for a canary test.
372struct CheatCanaryArgs {
373 bait: String,
374 tripwire: String,
375}
376
377impl Default for CheatCanaryArgs {
378 fn default() -> Self {
379 Self {
380 bait: "UNSPECIFIED".to_string(),
381 tripwire: "UNSPECIFIED".to_string(),
382 }
383 }
384}
385
386impl Parse for CheatCanaryArgs {
387 fn parse(input: ParseStream) -> syn::Result<Self> {
388 let mut args = CheatCanaryArgs::default();
389
390 let items: Punctuated<MetaItem, Token![,]> =
391 input.parse_terminated(MetaItem::parse, Token![,])?;
392
393 for item in items {
394 let key = item.key.to_string();
395 match (key.as_str(), item.value) {
396 ("bait", MetaValue::Str(s)) => args.bait = s,
397 ("tripwire", MetaValue::Str(s)) => args.tripwire = s,
398 (key, _) => {
399 return Err(syn::Error::new_spanned(
400 item.key,
401 format!("unknown key '{}'. Valid keys: bait, tripwire", key),
402 ))
403 }
404 }
405 }
406
407 Ok(args)
408 }
409}
410
411/// Mark a test as a canary - an intentionally verbose/tedious test that
412/// should trigger extra scrutiny if modified.
413///
414/// Canary tests are designed to be "bait" for cheating - they look like
415/// easy targets for simplification but modifications should trigger
416/// additional review.
417///
418/// # Attributes
419///
420/// - `bait` - Why this test looks tempting to simplify (string)
421/// - `tripwire` - What happens when this test is modified (string)
422///
423/// # Detection Mechanism
424///
425/// CI should check if files containing `#[cheat_canary]` tests are modified:
426/// - Add `[CANARY TRIGGERED]` label to PR
427/// - Require additional reviewer approval
428/// - Flag for human review
429///
430/// # Example
431///
432/// ```rust,ignore
433/// #[cheat_canary(
434/// bait = "This test looks tedious and tempts simplification to a loop",
435/// tripwire = "Any modification triggers full audit of all test changes"
436/// )]
437/// #[test]
438/// fn canary_verbose_binary_check() {
439/// // Intentionally verbose - checks each binary individually
440/// // A cheater would want to simplify this to a loop
441/// assert!(exists("/usr/bin/ls"), "ls missing");
442/// assert!(exists("/usr/bin/cat"), "cat missing");
443/// assert!(exists("/usr/bin/mount"), "mount missing");
444/// // ... many more individual assertions
445/// }
446/// ```
447///
448/// # Why Canaries Work
449///
450/// 1. They look like easy wins for "cleanup" or "refactoring"
451/// 2. But any change to them is suspicious by definition
452/// 3. The tripwire creates asymmetric cost: cheating is more expensive than honest work
453#[proc_macro_attribute]
454pub fn cheat_canary(args: TokenStream, input: TokenStream) -> TokenStream {
455 let args = parse_macro_input!(args as CheatCanaryArgs);
456 let input_fn = parse_macro_input!(input as ItemFn);
457
458 let fn_name = &input_fn.sig.ident;
459 let fn_name_str = fn_name.to_string();
460 let fn_attrs = &input_fn.attrs;
461 let fn_vis = &input_fn.vis;
462 let fn_block = &input_fn.block;
463 let fn_asyncness = &input_fn.sig.asyncness;
464
465 let bait = &args.bait;
466 let tripwire = &args.tripwire;
467
468 let expanded = quote! {
469 #(#fn_attrs)*
470 #fn_vis #fn_asyncness fn #fn_name() {
471 // CANARY TEST - Modifications to this test trigger extra scrutiny
472 // Bait: #bait
473 // Tripwire: #tripwire
474 //
475 // This comment is intentionally verbose. Do not remove or simplify.
476 // The canary detection system monitors this file for changes.
477 let _canary_test_name = #fn_name_str;
478 let _canary_bait = #bait;
479 let _canary_tripwire = #tripwire;
480
481 #fn_block
482 }
483 };
484
485 TokenStream::from(expanded)
486}