cli

Command-line interface for Radroots
git clone https://radroots.dev/git/cli.git
Log | Files | Refs | README | LICENSE

commit 21f4f39e2bb2ab5b40b693fb93f524433f38cb18
parent 1e5bb50839fb3c7265959639b9b32e9c3c58667f
Author: triesap <tyson@radroots.org>
Date:   Wed,  1 Jul 2026 04:50:50 +0000

tests: scan CLI production SDK surfaces

- add repo-wide removed SDK surface scanning
- strip cfg-test modules before production checks
- report removed surface findings with file lines
- keep migrated path guard coverage intact

Diffstat:
Msrc/runtime/sdk.rs | 330+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 330 insertions(+), 0 deletions(-)

diff --git a/src/runtime/sdk.rs b/src/runtime/sdk.rs @@ -1091,6 +1091,12 @@ mod tests { "trade_validation", ]; + const REMOVED_SDK_STATUS_SURFACE_TOKENS: &[&str] = &[ + "status_client(", + "TradeStatusClient", + "TradeValidationClient", + ]; + #[test] fn maps_runtime_config_to_sdk_builder_inputs() { let root = tempdir().expect("tempdir"); @@ -1308,6 +1314,80 @@ mod tests { } } + #[test] + fn cli_production_sources_reject_removed_sdk_status_surfaces_repo_wide() { + let manifest_dir = Path::new(env!("CARGO_MANIFEST_DIR")); + let mut files = Vec::new(); + collect_rs_files(manifest_dir.join("src").as_path(), &mut files); + files.sort(); + + let findings = files + .iter() + .flat_map(|file| { + let source = fs::read_to_string(file).expect("read cli source"); + let relative_path = relative_source_path(manifest_dir, file.as_path()); + let production_source = production_source_without_tests(&source); + let mut findings = + removed_sdk_status_surface_findings(&relative_path, production_source.as_str()); + findings.extend(root_trade_alias_findings( + &relative_path, + production_source.as_str(), + )); + findings + }) + .collect::<Vec<_>>(); + + assert!( + findings.is_empty(), + "CLI production sources contain removed SDK surfaces:\n{}", + findings.join("\n") + ); + } + + #[test] + fn cli_production_source_scanner_strips_test_modules() { + let inline = concat!( + "fn production() {}\n", + "#[cfg(test)] mod tests { fn test_only() { sdk.status_client(); } }\n", + ); + let multiline = concat!( + "fn production() {}\n", + "#[cfg(test)]\n", + "#[allow(dead_code)]\n", + "mod tests {\n", + " fn test_only() { let _ = TradeValidationClient; }\n", + " const BRACE: &str = \"}\";\n", + "}\n", + ); + + for source in [inline, multiline] { + let production_source = production_source_without_tests(source); + assert!( + removed_sdk_status_surface_findings("fixture.rs", production_source.as_str()) + .is_empty() + ); + assert!(root_trade_alias_findings("fixture.rs", production_source.as_str()).is_empty()); + } + } + + #[test] + fn repo_wide_removed_surface_scanner_reports_production_violations() { + let source = "fn production() { sdk.status_client(); RadrootsClient::trade_resync(&sdk); }"; + let status_findings = removed_sdk_status_surface_findings("fixture.rs", source); + let alias_findings = root_trade_alias_findings("fixture.rs", source); + + assert!( + status_findings + .iter() + .any(|finding| finding.contains("status_client(")) + ); + assert!( + alias_findings + .iter() + .any(|finding| finding.contains("trade_resync")) + ); + } + fn collect_rs_files(dir: &Path, files: &mut Vec<PathBuf>) { for entry in fs::read_dir(dir).expect("read dir") { let path = entry.expect("entry").path(); @@ -1452,6 +1532,256 @@ mod tests { findings } + fn removed_sdk_status_surface_findings(label: &str, source: &str) -> Vec<String> { + REMOVED_SDK_STATUS_SURFACE_TOKENS + .iter() + .flat_map(|token| { + source.match_indices(token).map(move |(index, _)| { + format!( + "{label}:{} uses removed SDK surface `{token}`", + line_number(source, index) + ) + }) + }) + .collect() + } + + fn production_source_without_tests(source: &str) -> String { + let mut production_source = String::with_capacity(source.len()); + let mut cursor = 0; + + while let Some((attribute_start, attribute_end)) = find_cfg_test_attribute(source, cursor) { + let Some(module_end) = find_cfg_test_module_end(source, attribute_end) else { + production_source.push_str(&source[cursor..attribute_end]); + cursor = attribute_end; + continue; + }; + + production_source.push_str(&source[cursor..attribute_start]); + for character in source[attribute_start..module_end].chars() { + if character == '\n' { + production_source.push('\n'); + } + } + cursor = module_end; + } + + production_source.push_str(&source[cursor..]); + production_source + } + + fn find_cfg_test_attribute(source: &str, start: usize) -> Option<(usize, usize)> { + let mut cursor = start; + while let Some(relative_start) = source[cursor..].find("#[") { + let attribute_start = cursor + relative_start; + let content_start = attribute_start + 2; + let content_end = source[content_start..] + .find(']') + .map(|relative_end| content_start + relative_end)?; + let normalized = source[content_start..content_end] + .chars() + .filter(|character| !character.is_whitespace()) + .collect::<String>(); + let attribute_end = content_end + 1; + if normalized == "cfg(test)" { + return Some((attribute_start, attribute_end)); + } + cursor = attribute_end; + } + None + } + + fn find_cfg_test_module_end(source: &str, attribute_end: usize) -> Option<usize> { + let mut cursor = skip_rust_whitespace(source, attribute_end); + while source[cursor..].starts_with("#[") { + let attribute_end = source[cursor..].find(']').map(|end| cursor + end + 1)?; + cursor = skip_rust_whitespace(source, attribute_end); + } + + cursor = skip_optional_visibility(source, cursor); + if !starts_with_rust_keyword(source, cursor, "mod") { + return None; + } + cursor = skip_rust_whitespace(source, cursor + "mod".len()); + cursor = skip_rust_identifier(source, cursor)?; + cursor = skip_rust_whitespace(source, cursor); + + if source[cursor..].starts_with(';') { + return Some(cursor + 1); + } + if !source[cursor..].starts_with('{') { + return None; + } + + find_matching_rust_brace(source, cursor).or(Some(source.len())) + } + + fn skip_optional_visibility(source: &str, cursor: usize) -> usize { + if !starts_with_rust_keyword(source, cursor, "pub") { + return cursor; + } + + let mut cursor = skip_rust_whitespace(source, cursor + "pub".len()); + if source[cursor..].starts_with('(') { + cursor = skip_balanced_parentheses(source, cursor).unwrap_or(cursor); + cursor = skip_rust_whitespace(source, cursor); + } + cursor + } + + fn skip_balanced_parentheses(source: &str, open_index: usize) -> Option<usize> { + let mut depth = 0usize; + for (relative_index, character) in source[open_index..].char_indices() { + match character { + '(' => depth += 1, + ')' => { + depth = depth.checked_sub(1)?; + if depth == 0 { + return Some(open_index + relative_index + character.len_utf8()); + } + } + _ => {} + } + } + None + } + + fn skip_rust_whitespace(source: &str, mut cursor: usize) -> usize { + while cursor < source.len() { + let Some(character) = source[cursor..].chars().next() else { + return cursor; + }; + if !character.is_whitespace() { + return cursor; + } + cursor += character.len_utf8(); + } + cursor + } + + fn skip_rust_identifier(source: &str, cursor: usize) -> Option<usize> { + let mut end = cursor; + let mut chars = source[cursor..].char_indices(); + let (_, first) = chars.next()?; + if first != '_' && !first.is_ascii_alphabetic() { + return None; + } + end += first.len_utf8(); + for (relative_index, character) in chars { + if character != '_' && !character.is_ascii_alphanumeric() { + return Some(cursor + relative_index); + } + end = cursor + relative_index + character.len_utf8(); + } + Some(end) + } + + fn starts_with_rust_keyword(source: &str, cursor: usize, keyword: &str) -> bool { + source[cursor..].starts_with(keyword) + && source[cursor + keyword.len()..] + .chars() + .next() + .is_none_or(|character| !is_rust_identifier_character(character)) + } + + fn find_matching_rust_brace(source: &str, open_index: usize) -> Option<usize> { + let bytes = source.as_bytes(); + let mut cursor = open_index; + let mut depth = 0usize; + + while cursor < bytes.len() { + match bytes[cursor] { + b'{' => { + depth += 1; + cursor += 1; + } + b'}' => { + depth = depth.checked_sub(1)?; + cursor += 1; + if depth == 0 { + return Some(cursor); + } + } + b'"' => cursor = skip_quoted_rust_literal(source, cursor, b'"')?, + b'\'' + if bytes + .get(cursor + 1) + .is_none_or(|byte| !byte.is_ascii_alphabetic() && *byte != b'_') => + { + cursor = skip_quoted_rust_literal(source, cursor, b'\'')? + } + b'/' if bytes.get(cursor + 1) == Some(&b'/') => { + cursor = source[cursor..] + .find('\n') + .map_or(source.len(), |newline| cursor + newline + 1); + } + b'/' if bytes.get(cursor + 1) == Some(&b'*') => { + cursor = source[cursor + 2..] + .find("*/") + .map(|end| cursor + 2 + end + 2)?; + } + b'r' => { + cursor = skip_raw_rust_string(source, cursor).unwrap_or(cursor + 1); + } + _ => cursor += 1, + } + } + + None + } + + fn skip_quoted_rust_literal(source: &str, start: usize, delimiter: u8) -> Option<usize> { + let bytes = source.as_bytes(); + let mut cursor = start + 1; + let mut escaped = false; + while cursor < bytes.len() { + let byte = bytes[cursor]; + if escaped { + escaped = false; + } else if byte == b'\\' { + escaped = true; + } else if byte == delimiter { + return Some(cursor + 1); + } + cursor += 1; + } + None + } + + fn skip_raw_rust_string(source: &str, start: usize) -> Option<usize> { + let bytes = source.as_bytes(); + let mut cursor = start + 1; + let mut hashes = 0usize; + + while bytes.get(cursor) == Some(&b'#') { + hashes += 1; + cursor += 1; + } + + if bytes.get(cursor) != Some(&b'"') { + return None; + } + cursor += 1; + + while cursor < bytes.len() { + if bytes[cursor] == b'"' { + let mut matched = true; + for offset in 0..hashes { + if bytes.get(cursor + 1 + offset) != Some(&b'#') { + matched = false; + break; + } + } + if matched { + return Some(cursor + 1 + hashes); + } + } + cursor += 1; + } + + None + } + fn is_rust_identifier_character(character: char) -> bool { character == '_' || character.is_ascii_alphanumeric() }