commit 0477bfd5b0cd2d6f0e6a54e7325c6dad06cd9ea6
parent ae53d4a6a319b553e942298d86aee0846e908d11
Author: triesap <tyson@radroots.org>
Date: Tue, 30 Jun 2026 21:16:25 +0000
tests: guard trade feature matrix
- prove runtime trade APIs without buyer write handles
- cover seller inbox in the runtime public API test
- split runtime and signer trade export guard expectations
- assert buyer and seller mutation cfg placement directly
Diffstat:
2 files changed, 167 insertions(+), 24 deletions(-)
diff --git a/crates/sdk/tests/source_boundary.rs b/crates/sdk/tests/source_boundary.rs
@@ -90,12 +90,33 @@ const REQUIRED_SDK_README_CONCEPTS: &[&str] = &[
];
const REQUIRED_TRADE_RUNTIME_EXPORTS: &[&str] = &[
+ "TRADE_STATUS_DEFAULT_LIMIT",
+ "TRADE_STATUS_MAX_LIMIT",
+ "TradeEvidenceIngestReceipt",
+ "TradeEvidenceIngestRequest",
+ "TradeRequestEvidenceIngestReceipt",
+ "TradeRequestEvidenceIngestRequest",
+ "TradeResyncReceipt",
+ "TradeResyncRequest",
+ "TradeSellerInboxReceipt",
+ "TradeSellerInboxRequest",
+ "TradeStatusAmbiguityCandidate",
+ "TradeStatusEligibility",
+ "TradeStatusEvidenceSummary",
+ "TradeStatusKind",
+ "TradeStatusNextActionKind",
+ "TradeStatusReceipt",
+ "TradeStatusRequest",
+ "SdkTradeStatusIssue",
+ "SdkTradeStatusIssueKind",
+ "SdkTradeStatusSource",
+];
+
+const REQUIRED_TRADE_SIGNER_EXPORTS: &[&str] = &[
"TRADE_CANCELLATION_OPERATION_KIND",
"TRADE_DECISION_OPERATION_KIND",
"TRADE_REVISION_DECISION_OPERATION_KIND",
"TRADE_REVISION_PROPOSAL_OPERATION_KIND",
- "TRADE_STATUS_DEFAULT_LIMIT",
- "TRADE_STATUS_MAX_LIMIT",
"TRADE_SUBMIT_OPERATION_KIND",
"TradeAcceptRequest",
"TradeCancelRequest",
@@ -104,29 +125,14 @@ const REQUIRED_TRADE_RUNTIME_EXPORTS: &[&str] = &[
"TradeDecisionPlan",
"TradeDecisionReceipt",
"TradeDeclineRequest",
- "TradeEvidenceIngestReceipt",
- "TradeEvidenceIngestRequest",
"TradeMutationOutcome",
"TradeProposeRequest",
- "TradeRequestEvidenceIngestReceipt",
- "TradeRequestEvidenceIngestRequest",
- "TradeResyncReceipt",
- "TradeResyncRequest",
"TradeRevisionDecisionPlan",
"TradeRevisionDecisionReceipt",
"TradeRevisionDecisionRequest",
"TradeRevisionProposalPlan",
"TradeRevisionProposalRequest",
"TradeRevisionProposalReceipt",
- "TradeSellerInboxReceipt",
- "TradeSellerInboxRequest",
- "TradeStatusAmbiguityCandidate",
- "TradeStatusEligibility",
- "TradeStatusEvidenceSummary",
- "TradeStatusKind",
- "TradeStatusNextActionKind",
- "TradeStatusReceipt",
- "TradeStatusRequest",
"TradeSubmitPlan",
"TradeSubmitReceipt",
"TradeWorkflowEnqueueReceipt",
@@ -134,9 +140,6 @@ const REQUIRED_TRADE_RUNTIME_EXPORTS: &[&str] = &[
"TradeWorkflowKind",
"TradeWorkflowPlan",
"TradeWorkflowRetryAdvice",
- "SdkTradeStatusIssue",
- "SdkTradeStatusIssueKind",
- "SdkTradeStatusSource",
];
const REQUIRED_DVM_RUNTIME_EXPORTS: &[&str] = &[
@@ -549,13 +552,33 @@ fn order_runtime_public_exports_are_explicit() {
"src/lib.rs must not wildcard-export the order runtime"
);
+ let runtime_exports = export_block(
+ source.as_str(),
+ "#[cfg(feature = \"runtime\")]\npub use crate::orders_runtime::{",
+ );
+ let signer_exports = export_block(
+ source.as_str(),
+ "#[cfg(all(feature = \"runtime\", feature = \"signer-adapters\"))]\npub use crate::orders_runtime::{",
+ );
+
for export in REQUIRED_TRADE_RUNTIME_EXPORTS {
assert!(
- source.contains(export),
+ runtime_exports.contains(export),
"src/lib.rs must explicitly expose trade SDK runtime export `{export}`"
);
}
+ for export in REQUIRED_TRADE_SIGNER_EXPORTS {
+ assert!(
+ signer_exports.contains(export),
+ "src/lib.rs must expose trade SDK signer export `{export}` behind runtime plus signer-adapters"
+ );
+ assert!(
+ !runtime_exports.contains(export),
+ "src/lib.rs runtime-only export block must not expose signer workflow export `{export}`"
+ );
+ }
+
for export in REQUIRED_TRADE_POLICY_EXPORTS {
assert!(
source.contains(export),
@@ -715,6 +738,67 @@ fn trade_product_facade_methods_are_inventory_guarded() {
}
#[test]
+fn trade_product_facade_feature_gates_are_explicit() {
+ let orders_source = read_source(
+ Path::new(env!("CARGO_MANIFEST_DIR"))
+ .join("src/orders_runtime.rs")
+ .as_path(),
+ );
+ let product_clients_source = read_source(
+ Path::new(env!("CARGO_MANIFEST_DIR"))
+ .join("src/product_clients.rs")
+ .as_path(),
+ );
+
+ assert!(
+ product_clients_source.contains(
+ "#[cfg(feature = \"signer-adapters\")]\n pub fn buyer(&self) -> TradeBuyerClient<'client> {"
+ ),
+ "TradesClient::buyer must be gated by signer-adapters inside the runtime TradesClient impl"
+ );
+ assert!(
+ product_clients_source.contains(
+ "#[cfg(all(feature = \"runtime\", feature = \"signer-adapters\"))]\n#[derive(Clone, Copy)]\npub struct TradeBuyerClient<'client>"
+ ),
+ "TradeBuyerClient must be gated by runtime plus signer-adapters"
+ );
+ assert!(
+ product_clients_source.contains(
+ "#[cfg(feature = \"runtime\")]\n#[derive(Clone, Copy)]\npub struct TradeSellerClient<'client>"
+ ),
+ "TradeSellerClient must remain runtime-visible for seller inbox reads"
+ );
+ assert!(
+ orders_source.contains(
+ "#[cfg(all(feature = \"runtime\", feature = \"signer-adapters\"))]\nimpl<'sdk> TradeBuyerClient<'sdk> {"
+ ),
+ "TradeBuyerClient product mutation impl must require runtime plus signer-adapters"
+ );
+ assert!(
+ orders_source
+ .contains("#[cfg(feature = \"runtime\")]\nimpl<'sdk> TradeSellerClient<'sdk> {"),
+ "TradeSellerClient impl must remain runtime-visible for seller inbox"
+ );
+
+ let seller_impl = impl_block(
+ orders_source.as_str(),
+ "impl<'sdk> TradeSellerClient<'sdk> {",
+ );
+ assert!(
+ seller_impl.contains("pub async fn inbox("),
+ "TradeSellerClient must expose seller inbox in the runtime impl"
+ );
+ for method in ["accept_trade", "decline_trade", "propose_revision"] {
+ let gated_method =
+ format!("#[cfg(feature = \"signer-adapters\")]\n pub async fn {method}(");
+ assert!(
+ seller_impl.contains(gated_method.as_str()),
+ "TradeSellerClient::{method} must be gated by signer-adapters"
+ );
+ }
+}
+
+#[test]
fn dvm_client_surface_is_inventory_guarded() {
let source = read_source(
Path::new(env!("CARGO_MANIFEST_DIR"))
@@ -774,7 +858,6 @@ fn product_clients_remain_thin_sdk_handles() {
"ListingsClient",
"MarketClient",
"SyncClient",
- "TradeBuyerClient",
"TradeResyncClient",
"TradeSellerClient",
"TradesClient",
@@ -789,6 +872,25 @@ fn product_clients_remain_thin_sdk_handles() {
);
}
+ let runtime_product_exports = export_block(
+ lib_source.as_str(),
+ "#[cfg(feature = \"runtime\")]\npub use crate::product_clients::{",
+ );
+ assert!(
+ !runtime_product_exports.contains("TradeBuyerClient"),
+ "src/lib.rs runtime-only product client export block must not expose TradeBuyerClient"
+ );
+ assert!(
+ lib_source.contains(
+ "#[cfg(all(feature = \"runtime\", feature = \"signer-adapters\"))]\npub use crate::product_clients::TradeBuyerClient;"
+ ),
+ "src/lib.rs must export TradeBuyerClient only behind runtime plus signer-adapters"
+ );
+ assert!(
+ clients_source.contains("pub struct TradeBuyerClient<'client>"),
+ "product_clients.rs must define thin signer-gated handle `TradeBuyerClient`"
+ );
+
for forbidden in FORBIDDEN_PRODUCT_CLIENT_HANDLES {
assert!(
!lib_source.contains(forbidden),
@@ -933,6 +1035,28 @@ fn function_source<'source>(source: &'source str, function_name: &str) -> &'sour
&source_after_start[..end]
}
+fn export_block<'source>(source: &'source str, marker: &str) -> &'source str {
+ let start = source
+ .find(marker)
+ .unwrap_or_else(|| panic!("failed to find export block marker `{marker}`"));
+ let source_after_start = &source[start..];
+ let end = source_after_start
+ .find("\n};")
+ .unwrap_or_else(|| panic!("failed to find end of export block `{marker}`"));
+ &source_after_start[..end]
+}
+
+fn impl_block<'source>(source: &'source str, marker: &str) -> &'source str {
+ let start = source
+ .find(marker)
+ .unwrap_or_else(|| panic!("failed to find impl block marker `{marker}`"));
+ let source_after_start = &source[start..];
+ let end = source_after_start
+ .find("\n}\n\n#[cfg(")
+ .unwrap_or(source_after_start.len());
+ &source_after_start[..end]
+}
+
fn is_rust_identifier_character(character: char) -> bool {
character == '_' || character.is_ascii_alphanumeric()
}
diff --git a/crates/sdk/tests/trade_public_api.rs b/crates/sdk/tests/trade_public_api.rs
@@ -1,13 +1,20 @@
#![cfg(feature = "runtime")]
-use radroots_sdk::{RadrootsClient, TradeResyncRequest, TradeStatusKind, TradeStatusRequest};
+use radroots_authority::RadrootsActorContext;
+use radroots_events::contract::RadrootsActorRole;
+use radroots_sdk::{
+ RadrootsClient, TradeResyncRequest, TradeSellerInboxRequest, TradeStatusKind,
+ TradeStatusRequest,
+};
+
+const SELLER_PUBLIC_KEY_HEX: &str =
+ "e0266e3cfb0d2886f91c73f5f868f3b98273713e5fcd97c081663f5518a4b3af";
#[tokio::test]
async fn grouped_trade_surface_is_the_public_product_entrypoint() {
let sdk = RadrootsClient::builder().build().await.expect("sdk");
let trades = sdk.trades();
- let _buyer = trades.buyer();
let _seller = trades.seller();
let _resync = trades.resync();
@@ -25,4 +32,16 @@ async fn grouped_trade_surface_is_the_public_product_entrypoint() {
.expect("resync");
assert_eq!(resync.status.status, TradeStatusKind::Missing);
+
+ let seller_actor =
+ RadrootsActorContext::test(SELLER_PUBLIC_KEY_HEX, [RadrootsActorRole::Seller])
+ .expect("seller actor");
+ let inbox = trades
+ .seller()
+ .inbox(TradeSellerInboxRequest::new(seller_actor))
+ .await
+ .expect("seller inbox");
+
+ assert_eq!(inbox.seller_pubkey.as_str(), SELLER_PUBLIC_KEY_HEX);
+ assert!(inbox.statuses.is_empty());
}