commit fd39b32a227663bd0cf1df041b7871db39cedf28
parent 2d355e35d70500c6017ae12327e6c08b9ab230c4
Author: triesap <tyson@radroots.org>
Date: Tue, 30 Jun 2026 23:57:07 +0000
tests: harden Studio SDK surface guards
- reject removed status client surfaces
- guard SDK root trade alias calls
- keep AppSdkRuntime as the production boundary
- prove the Studio source guard lane
Diffstat:
1 file changed, 113 insertions(+), 0 deletions(-)
diff --git a/crates/desktop/src/source_guards.rs b/crates/desktop/src/source_guards.rs
@@ -1134,6 +1134,18 @@ const STRICT_SDK_BOUNDARY_FORBIDDEN_PATTERNS: &[SdkBoundaryForbiddenPattern] = &
reason: "app production sources must use AppSdkConfig-derived runtime construction",
},
SdkBoundaryForbiddenPattern {
+ pattern: "status_client(",
+ reason: "app production sources must use AppSdkRuntime trade status methods instead of removed SDK status clients",
+ },
+ SdkBoundaryForbiddenPattern {
+ pattern: "TradeStatusClient",
+ reason: "app production sources must use AppSdkRuntime trade status methods instead of removed SDK status handles",
+ },
+ SdkBoundaryForbiddenPattern {
+ pattern: "TradeValidationClient",
+ reason: "app production sources must use AppSdkRuntime DVM methods instead of removed SDK validation handles",
+ },
+ SdkBoundaryForbiddenPattern {
pattern: "SdkTransportMode::RelayDirect",
reason: "app production sources must not configure direct relay publish transport",
},
@@ -1279,6 +1291,14 @@ const STRICT_SDK_BOUNDARY_FORBIDDEN_PATTERNS: &[SdkBoundaryForbiddenPattern] = &
},
];
+const FORBIDDEN_SDK_ROOT_TRADE_ALIAS_NAMES: &[&str] = &[
+ "trade_buyer",
+ "trade_seller",
+ "trade_status",
+ "trade_resync",
+ "trade_validation",
+];
+
const SDK_BOUNDARY_EXCEPTIONS: &[SdkBoundaryExceptionEntry] = &[
SdkBoundaryExceptionEntry {
path: "crates/desktop/src/accounts.rs",
@@ -1513,6 +1533,15 @@ fn app_production_sdk_boundary_usage_is_exception_scoped() {
findings.first().map_or("", |finding| finding.pattern),
findings.first().map_or("", |finding| finding.reason)
);
+
+ let root_alias_findings =
+ sdk_root_trade_alias_findings(relative_path.as_str(), production_source);
+ assert!(
+ root_alias_findings.is_empty(),
+ "{} contains removed SDK root trade alias usage:\n{}",
+ relative_path,
+ root_alias_findings.join("\n")
+ );
}
}
@@ -1585,6 +1614,21 @@ fn strict_sdk_boundary_scanner_rejects_unexcepted_new_production_paths() {
);
assert_eq!(runtime_findings.len(), 1);
assert_eq!(runtime_findings[0].pattern, "RadrootsSdkClient");
+ let status_findings = unexcepted_sdk_boundary_patterns(
+ "crates/desktop/src/runtime.rs",
+ "fn status() { let _ = TradeStatusClient::new(root); root.status_client(); }",
+ );
+ assert_eq!(status_findings.len(), 2);
+ assert!(
+ status_findings
+ .iter()
+ .any(|finding| finding.pattern == "TradeStatusClient")
+ );
+ assert!(
+ status_findings
+ .iter()
+ .any(|finding| finding.pattern == "status_client(")
+ );
assert!(
unexcepted_sdk_boundary_patterns(
"crates/desktop/src/accounts.rs",
@@ -1595,6 +1639,27 @@ fn strict_sdk_boundary_scanner_rejects_unexcepted_new_production_paths() {
}
#[test]
+fn strict_sdk_boundary_scanner_rejects_removed_root_trade_alias_calls() {
+ let allowed_findings = sdk_root_trade_alias_findings(
+ "crates/runtime/src/sdk.rs",
+ "pub fn trade_status(&self) {} fn trade_status_for_locator() {}",
+ );
+ assert!(allowed_findings.is_empty());
+
+ let findings = sdk_root_trade_alias_findings(
+ "crates/desktop/src/runtime.rs",
+ "sdk.trade_status (request); RadrootsClient::trade_resync(&sdk);",
+ );
+
+ for alias in ["trade_status", "trade_resync"] {
+ assert!(
+ findings.iter().any(|finding| finding.contains(alias)),
+ "strict SDK boundary scanner must reject `{alias}`"
+ );
+ }
+}
+
+#[test]
fn app_sdk_boundary_exception_entries_are_complete_and_current() {
let app_root = app_root();
let mut entries = BTreeSet::new();
@@ -1692,6 +1757,42 @@ fn sdk_boundary_exception_contains(path: &str, pattern: &str) -> bool {
.any(|entry| entry.path == path && entry.pattern == pattern)
}
+fn sdk_root_trade_alias_findings(path: &str, production_source: &str) -> Vec<String> {
+ let mut findings = Vec::new();
+
+ for alias in FORBIDDEN_SDK_ROOT_TRADE_ALIAS_NAMES {
+ for (index, _) in production_source.match_indices(alias) {
+ let before = production_source[..index].chars().next_back();
+ let after_index = index + alias.len();
+ let after = production_source[after_index..].chars().next();
+
+ if before.is_some_and(is_rust_identifier_character)
+ || after.is_some_and(is_rust_identifier_character)
+ {
+ continue;
+ }
+
+ if production_source[after_index..]
+ .chars()
+ .find(|character| !character.is_whitespace())
+ != Some('(')
+ {
+ continue;
+ }
+
+ let prefix = production_source[..index].trim_end();
+ if prefix.ends_with('.') || prefix.ends_with("::") {
+ findings.push(format!(
+ "{path}:{} uses removed SDK root trade alias `{alias}`",
+ line_number(production_source, index)
+ ));
+ }
+ }
+ }
+
+ findings
+}
+
fn read_source_path(path: &Path) -> String {
fs::read_to_string(path)
.unwrap_or_else(|error| panic!("failed to read source {}: {error}", path.display()))
@@ -1743,6 +1844,18 @@ fn is_rust_identifier_byte(byte: u8) -> bool {
byte.is_ascii_alphanumeric() || byte == b'_'
}
+fn is_rust_identifier_character(character: char) -> bool {
+ character == '_' || character.is_ascii_alphanumeric()
+}
+
+fn line_number(source: &str, index: usize) -> usize {
+ source[..index]
+ .bytes()
+ .filter(|byte| *byte == b'\n')
+ .count()
+ + 1
+}
+
fn contains_reserved_payment_action_term(value: &str, term: &str) -> bool {
if term.contains(' ') || term.contains('-') {
return value.contains(term);