commit 8714874e201b1f9449fbd5a2664163056980d39c
parent 0cc2657a14239b6af66ac743a218c636cbe91cf9
Author: triesap <tyson@radroots.org>
Date: Tue, 7 Jul 2026 06:27:59 +0000
transport: add daemon source boundary guards
- add daemon transport source-boundary coverage for removed publish proxy identifiers
- guard Reticulum preview endpoint identity against old reticulum:preview usage
- require daemon capability tests and protocol fields to expose readiness and usability
- validate with fmt, daemon check, source-boundary tests, and transport tests
Diffstat:
1 file changed, 180 insertions(+), 0 deletions(-)
diff --git a/tests/source_boundary.rs b/tests/source_boundary.rs
@@ -0,0 +1,180 @@
+use std::{
+ fs,
+ path::{Path, PathBuf},
+};
+
+struct ForbiddenConcept {
+ pattern: &'static str,
+ reason: &'static str,
+}
+
+const FORBIDDEN_DAEMON_TRANSPORT_CONCEPTS: &[ForbiddenConcept] = &[
+ ForbiddenConcept {
+ pattern: "radrootsd.publish_proxy.v1",
+ reason: "daemon publish proxy v1 is removed",
+ },
+ ForbiddenConcept {
+ pattern: "publish.relays.resolve",
+ reason: "relay-resolution RPC is replaced by transport publish target policy",
+ },
+ ForbiddenConcept {
+ pattern: "\"publish.event\"",
+ reason: "publish.event is replaced by transport.publish.event",
+ },
+ ForbiddenConcept {
+ pattern: "transport_kinds",
+ reason: "capabilities must expose per-transport readiness instead of kind-only lists",
+ },
+ ForbiddenConcept {
+ pattern: "allowed_relay_policy",
+ reason: "relay policy is Nostr-specific and must not be a generic transport API",
+ },
+ ForbiddenConcept {
+ pattern: "relay_policy",
+ reason: "relay policy is Nostr-specific and must not be a generic transport API",
+ },
+ ForbiddenConcept {
+ pattern: "PublishRelayPolicy",
+ reason: "old relay-shaped publish policy names must not return",
+ },
+ ForbiddenConcept {
+ pattern: "PublishRelayOutcome",
+ reason: "old relay-shaped publish outcome names must not return",
+ },
+ ForbiddenConcept {
+ pattern: "PublishRelaySource",
+ reason: "old relay-shaped publish source names must not return",
+ },
+];
+
+#[test]
+fn transport_publish_sources_reject_removed_protocol_identifiers() {
+ let manifest_dir = Path::new(env!("CARGO_MANIFEST_DIR"));
+ let src_dir = manifest_dir.join("src");
+ let mut findings = Vec::new();
+
+ for path in rust_source_files(src_dir.as_path()) {
+ let source = read_source(path.as_path());
+ let relative_path = relative_path(manifest_dir, path.as_path());
+
+ for concept in FORBIDDEN_DAEMON_TRANSPORT_CONCEPTS {
+ if contains_forbidden_concept(source.as_str(), concept.pattern) {
+ findings.push(format!(
+ "{} contains removed daemon transport concept `{}`: {}",
+ relative_path, concept.pattern, concept.reason
+ ));
+ }
+ }
+
+ for line in removed_reticulum_preview_endpoint_lines(source.as_str()) {
+ findings.push(format!(
+ "{relative_path}:{line} contains removed Reticulum preview endpoint `reticulum:preview`"
+ ));
+ }
+ }
+
+ assert!(
+ findings.is_empty(),
+ "daemon transport source-boundary violations:\n{}",
+ findings.join("\n")
+ );
+}
+
+#[test]
+fn transport_publish_capabilities_expose_per_transport_readiness() {
+ let methods_source = read_source(
+ Path::new(env!("CARGO_MANIFEST_DIR"))
+ .join("src/transport/jsonrpc/methods/mod.rs")
+ .as_path(),
+ );
+ for required in [
+ "transport.publish.capabilities",
+ r#"\"transport_kind\":\"reticulum\""#,
+ r#"\"implementation_state\":\"preview_unavailable\""#,
+ r#"\"usable_for_delivery\":false"#,
+ ] {
+ assert!(
+ methods_source.contains(required),
+ "daemon capabilities tests must retain transport readiness witness `{required}`"
+ );
+ }
+
+ let protocol_source = read_source(
+ Path::new(env!("CARGO_MANIFEST_DIR"))
+ .join("../lib/crates/transport_publish_protocol/src/lib.rs")
+ .as_path(),
+ );
+ for required in [
+ "pub implementation_state: TransportPublishImplementationState,",
+ "pub usable_for_delivery: bool,",
+ ] {
+ assert!(
+ protocol_source.contains(required),
+ "transport publish protocol must retain capability field `{required}`"
+ );
+ }
+}
+
+fn rust_source_files(root: &Path) -> Vec<PathBuf> {
+ let mut paths = Vec::new();
+ collect_rust_source_files(root, &mut paths);
+ paths.sort();
+ paths
+}
+
+fn collect_rust_source_files(root: &Path, paths: &mut Vec<PathBuf>) {
+ for entry in fs::read_dir(root)
+ .unwrap_or_else(|error| panic!("failed to read {}: {error}", root.display()))
+ {
+ let entry = entry.expect("read source entry");
+ let path = entry.path();
+ if path.is_dir() {
+ collect_rust_source_files(path.as_path(), paths);
+ } else if path.extension().and_then(|extension| extension.to_str()) == Some("rs") {
+ paths.push(path);
+ }
+ }
+}
+
+fn read_source(path: &Path) -> String {
+ fs::read_to_string(path)
+ .unwrap_or_else(|error| panic!("failed to read source {}: {error}", path.display()))
+}
+
+fn relative_path(root: &Path, path: &Path) -> String {
+ path.strip_prefix(root)
+ .expect("source path is under repo root")
+ .to_string_lossy()
+ .replace('\\', "/")
+}
+
+fn contains_forbidden_concept(source: &str, pattern: &str) -> bool {
+ source.match_indices(pattern).any(|(index, _)| {
+ let before = source[..index].chars().next_back();
+ let after = source[index + pattern.len()..].chars().next();
+ before.is_none_or(|character| !is_rust_identifier_character(character))
+ && after.is_none_or(|character| !is_rust_identifier_character(character))
+ })
+}
+
+fn removed_reticulum_preview_endpoint_lines(source: &str) -> Vec<usize> {
+ source
+ .match_indices("reticulum:preview")
+ .filter_map(|(index, _)| {
+ let after = source[index + "reticulum:preview".len()..].chars().next();
+ (after != Some('-')).then(|| line_number(source, index))
+ })
+ .collect()
+}
+
+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
+}