diff --git a/crates/sprout-audit/src/service.rs b/crates/sprout-audit/src/service.rs index 0a09bf641..15d845900 100644 --- a/crates/sprout-audit/src/service.rs +++ b/crates/sprout-audit/src/service.rs @@ -75,7 +75,6 @@ impl AuditService { .catch_unwind() .await; - // Always release the lock before returning the connection to the pool. let _ = sqlx::query("DO RELEASE_LOCK(?)") .bind(AUDIT_LOCK_NAME) .execute(&mut *conn) diff --git a/crates/sprout-auth/src/nip42.rs b/crates/sprout-auth/src/nip42.rs index 3502c9549..26ebb2711 100644 --- a/crates/sprout-auth/src/nip42.rs +++ b/crates/sprout-auth/src/nip42.rs @@ -27,7 +27,6 @@ fn normalize_relay_url(raw: &str) -> String { let _ = parsed.set_host(Some("127.0.0.1")); } } - // Remove trailing slash from the path component. let path = parsed.path().trim_end_matches('/').to_string(); parsed.set_path(&path); parsed.to_string() diff --git a/crates/sprout-auth/src/okta.rs b/crates/sprout-auth/src/okta.rs index 43363db41..35a40f8fa 100644 --- a/crates/sprout-auth/src/okta.rs +++ b/crates/sprout-auth/src/okta.rs @@ -204,7 +204,6 @@ impl JwksCache { ttl_secs: u64, client: &reqwest::Client, ) -> Result { - // Fast path: read lock, return if fresh. { let guard = self.inner.read().await; if let Some(cached) = guard.as_ref() { @@ -244,7 +243,6 @@ impl JwksCache { fetched_at: Instant::now(), }; - // Re-acquire write lock to store the result. // Final re-check: another thread may have stored a fresh entry while // we were fetching. If so, discard our result and return theirs. let mut guard = self.inner.write().await; diff --git a/crates/sprout-core/src/filter.rs b/crates/sprout-core/src/filter.rs index 5822c58e3..9544f3010 100644 --- a/crates/sprout-core/src/filter.rs +++ b/crates/sprout-core/src/filter.rs @@ -91,21 +91,18 @@ mod tests { let past = Timestamp::from(now_ts.as_u64() - 3600); let future = Timestamp::from(now_ts.as_u64() + 3600); - // kind assert!(filters_match(&[Filter::new().kind(Kind::TextNote)], &ev)); assert!(!filters_match( &[Filter::new().kind(Kind::ContactList)], &ev )); - // author assert!(filters_match(&[Filter::new().author(pubkey)], &ev)); assert!(!filters_match( &[Filter::new().author(Keys::generate().public_key())], &ev )); - // compound AND assert!(filters_match( &[Filter::new().kind(Kind::TextNote).author(pubkey)], &ev @@ -115,7 +112,6 @@ mod tests { &ev )); - // since / until assert!(filters_match(&[Filter::new().since(past)], &ev)); assert!(!filters_match(&[Filter::new().since(future)], &ev)); assert!(filters_match(&[Filter::new().until(future)], &ev)); diff --git a/crates/sprout-db/src/channel.rs b/crates/sprout-db/src/channel.rs index c472685a5..29cb8f868 100644 --- a/crates/sprout-db/src/channel.rs +++ b/crates/sprout-db/src/channel.rs @@ -204,9 +204,6 @@ pub async fn create_channel( let id = Uuid::new_v4(); let id_bytes = id.as_bytes().as_slice().to_vec(); - // Use a transaction so the INSERT + SELECT are atomic. Without this, a concurrent - // reader could see the channel between the insert and the fetch, or the channel - // could be modified before we read it back. let mut tx = pool.begin().await?; sqlx::query( @@ -329,9 +326,6 @@ pub async fn add_member( let channel_id_bytes = channel_id.as_bytes().as_slice().to_vec(); - // Begin transaction: all role checks and the INSERT run atomically. - // This prevents a TOCTOU race where the inviter is removed between the - // role check and the INSERT. let mut tx = pool.begin().await?; let channel = get_channel_tx(&mut tx, channel_id).await?; @@ -514,7 +508,6 @@ pub async fn get_members(pool: &MySqlPool, channel_id: Uuid) -> Result Result> { let rows = sqlx::query( r#" diff --git a/crates/sprout-db/src/feed.rs b/crates/sprout-db/src/feed.rs index a756346d1..2b6a13ffe 100644 --- a/crates/sprout-db/src/feed.rs +++ b/crates/sprout-db/src/feed.rs @@ -72,12 +72,10 @@ pub async fn query_mentions( .push_bind(serde_json::json!([["p", pubkey_hex]]).to_string()) .push(", '$')"); - // Kinds: stream messages, stream replies, forum posts, forum comments qb.push(format!( " AND kind IN ({KIND_STREAM_MESSAGE}, {KIND_STREAM_MESSAGE_V2}, {KIND_FORUM_POST}, {KIND_FORUM_COMMENT})" )); - // Channel access filter if !accessible_channel_ids.is_empty() { qb.push(" AND channel_id IN ("); let mut sep = qb.separated(", "); @@ -130,16 +128,12 @@ pub async fn query_needs_action( " AND kind IN ({KIND_WORKFLOW_APPROVAL_REQUESTED}, {KIND_STREAM_REMINDER})" )); - // Tag filter: must be tagged to this user. // Wrap in outer array so MySQL checks for exact sub-array membership — see // query_mentions for a full explanation of the JSON_CONTAINS semantics. qb.push(" AND JSON_CONTAINS(tags, ") .push_bind(serde_json::json!([["p", pubkey_hex]]).to_string()) .push(", '$')"); - // Access control: only return events from channels the user can access. - // Identical pattern to query_mentions — prevents leaking events from - // channels the user has been removed from. if !accessible_channel_ids.is_empty() { qb.push(" AND channel_id IN ("); let mut sep = qb.separated(", "); @@ -183,8 +177,6 @@ pub async fn query_activity( FROM events WHERE 1=1", ); - // Stream messages, forum posts, agent job events. - // KIND_JOB_REQUEST = agent job requested, KIND_JOB_PROGRESS = in-flight progress update, KIND_JOB_RESULT = completed result. qb.push(format!( " AND kind IN ({KIND_STREAM_MESSAGE}, {KIND_STREAM_MESSAGE_V2}, {KIND_FORUM_POST}, {KIND_JOB_REQUEST}, {KIND_JOB_PROGRESS}, {KIND_JOB_RESULT})" )); @@ -233,7 +225,6 @@ mod tests { #[test] fn pubkey_hex_encoding_32_byte_key() { - // Simulate a full 32-byte Nostr pubkey. let pubkey_bytes: Vec = (0u8..32).collect(); let hex = hex::encode(&pubkey_bytes); assert_eq!(hex.len(), 64); @@ -308,8 +299,6 @@ mod tests { use sprout_core::kind::{ KIND_FORUM_COMMENT, KIND_FORUM_POST, KIND_STREAM_MESSAGE, KIND_STREAM_MESSAGE_V2, }; - // query_mentions filters for: KIND_STREAM_MESSAGE, KIND_STREAM_MESSAGE_V2, - // KIND_FORUM_POST, KIND_FORUM_COMMENT let mention_kinds: &[u32] = &[ KIND_STREAM_MESSAGE, KIND_STREAM_MESSAGE_V2, @@ -338,7 +327,6 @@ mod tests { #[test] fn needs_action_query_includes_approval_and_reminder_kinds() { use sprout_core::kind::{KIND_STREAM_REMINDER, KIND_WORKFLOW_APPROVAL_REQUESTED}; - // query_needs_action filters for: KIND_WORKFLOW_APPROVAL_REQUESTED, KIND_STREAM_REMINDER let needs_action_kinds: &[u32] = &[KIND_WORKFLOW_APPROVAL_REQUESTED, KIND_STREAM_REMINDER]; assert!( @@ -357,8 +345,6 @@ mod tests { KIND_FORUM_POST, KIND_JOB_PROGRESS, KIND_JOB_REQUEST, KIND_JOB_RESULT, KIND_STREAM_MESSAGE, KIND_STREAM_MESSAGE_V2, }; - // query_activity filters for: KIND_STREAM_MESSAGE, KIND_STREAM_MESSAGE_V2, - // KIND_FORUM_POST, KIND_JOB_REQUEST, KIND_JOB_PROGRESS, KIND_JOB_RESULT let activity_kinds: &[u32] = &[ KIND_STREAM_MESSAGE, KIND_STREAM_MESSAGE_V2, @@ -423,7 +409,6 @@ mod tests { KIND_STREAM_MESSAGE, KIND_STREAM_MESSAGE_V2, KIND_STREAM_REMINDER, KIND_WORKFLOW_APPROVAL_REQUESTED, }; - // The two queries serve different purposes — their kind sets should not overlap. let needs_action_kinds: &[u32] = &[KIND_WORKFLOW_APPROVAL_REQUESTED, KIND_STREAM_REMINDER]; let activity_kinds: &[u32] = &[ KIND_STREAM_MESSAGE, diff --git a/crates/sprout-db/src/lib.rs b/crates/sprout-db/src/lib.rs index 5aa8d39cd..27d98f6ff 100644 --- a/crates/sprout-db/src/lib.rs +++ b/crates/sprout-db/src/lib.rs @@ -804,7 +804,6 @@ mod tests { assert_eq!(channel.description, Some("desc".to_string())); assert!(db.is_member(channel.id, &owner).await.unwrap()); - // Add member via owner invite db.add_member( channel.id, &member, @@ -818,7 +817,6 @@ mod tests { let members = db.get_members(channel.id).await.expect("get members"); assert_eq!(members.len(), 2); - // Owner removes member db.remove_member(channel.id, &member, &owner) .await .expect("remove"); @@ -919,17 +917,14 @@ mod tests { .await .expect("add rando"); - // Rando cannot remove member let result = db.remove_member(channel.id, &member, &rando).await; assert!(matches!(result, Err(DbError::AccessDenied(_)))); - // Owner can remove member db.remove_member(channel.id, &member, &owner) .await .expect("owner removes"); assert!(!db.is_member(channel.id, &member).await.unwrap()); - // Member can remove themselves db.remove_member(channel.id, &rando, &rando) .await .expect("self-remove"); diff --git a/crates/sprout-db/src/workflow.rs b/crates/sprout-db/src/workflow.rs index c6e9c3130..8ca362f95 100644 --- a/crates/sprout-db/src/workflow.rs +++ b/crates/sprout-db/src/workflow.rs @@ -940,14 +940,12 @@ mod tests { let mut cloned = record.clone(); cloned.name = "Cloned".to_owned(); - // Original is unchanged. assert_eq!(record.name, "Original"); assert_eq!(cloned.name, "Cloned"); } #[test] fn workflow_record_status_variants() { - // Verify all WorkflowStatus variants can be stored in the struct. let now = Utc::now(); for status in &[ WorkflowStatus::Active, @@ -1093,7 +1091,6 @@ mod tests { created_at: now, }; - // Trace is a JSON array with 2 entries. assert!(record.execution_trace.is_array()); assert_eq!(record.execution_trace.as_array().unwrap().len(), 2); } diff --git a/crates/sprout-huddle/src/lib.rs b/crates/sprout-huddle/src/lib.rs index d2c0bf14e..1eb3a2c6a 100644 --- a/crates/sprout-huddle/src/lib.rs +++ b/crates/sprout-huddle/src/lib.rs @@ -139,7 +139,6 @@ mod tests { HuddleService::create_room_name(id), "sprout-550e8400-e29b-41d4-a716-446655440000" ); - // Deterministic assert_eq!( HuddleService::create_room_name(id), HuddleService::create_room_name(id) diff --git a/crates/sprout-huddle/src/webhook.rs b/crates/sprout-huddle/src/webhook.rs index efd86e47e..835fc22dd 100644 --- a/crates/sprout-huddle/src/webhook.rs +++ b/crates/sprout-huddle/src/webhook.rs @@ -186,15 +186,12 @@ mod tests { #[test] fn test_webhook_event_variants() { - // room_started let ev = signed_parse(r#"{"event":"room_started","room":{"name":"r1"}}"#).unwrap(); assert_eq!(ev, WebhookEvent::RoomStarted { room: "r1".into() }); - // room_finished let ev = signed_parse(r#"{"event":"room_finished","room":{"name":"r1"}}"#).unwrap(); assert_eq!(ev, WebhookEvent::RoomFinished { room: "r1".into() }); - // participant_joined let ev = signed_parse( r#"{"event":"participant_joined","room":{"name":"r1"},"participant":{"identity":"alice"}}"#, ) @@ -207,7 +204,6 @@ mod tests { } ); - // participant_left let ev = signed_parse( r#"{"event":"participant_left","room":{"name":"r1"},"participant":{"identity":"alice"}}"#, ) @@ -220,7 +216,6 @@ mod tests { } ); - // track_published — audio (default) let ev = signed_parse( r#"{"event":"track_published","room":{"name":"r1"},"participant":{"identity":"alice"},"track":{"type":"audio"}}"#, ) @@ -234,7 +229,6 @@ mod tests { } ); - // track_published — video let ev = signed_parse( r#"{"event":"track_published","room":{"name":"r1"},"participant":{"identity":"alice"},"track":{"type":"video"}}"#, ) @@ -248,7 +242,6 @@ mod tests { } ); - // track_published — screen_share let ev = signed_parse( r#"{"event":"track_published","room":{"name":"r1"},"participant":{"identity":"alice"},"track":{"type":"screen_share"}}"#, ) diff --git a/crates/sprout-mcp/src/relay_client.rs b/crates/sprout-mcp/src/relay_client.rs index c9b98db0d..6d3b55383 100644 --- a/crates/sprout-mcp/src/relay_client.rs +++ b/crates/sprout-mcp/src/relay_client.rs @@ -435,13 +435,11 @@ impl RelayClient { let new_inner = Self::connect_with_retry(&self.relay_url, &self.keys, self.api_token.as_deref()).await; - // Swap the inner connection. { let mut inner = self.inner.lock().await; *inner = new_inner; } - // Resubscribe to all active subscriptions. let subs = self.active_subscriptions.lock().await.clone(); if !subs.is_empty() { tracing::info!("resubscribing to {} active subscription(s)", subs.len()); @@ -879,7 +877,6 @@ mod tests { let text = "not json at all"; let result = parse_relay_message(text); assert!(result.is_err()); - // Should be a JSON parse error. assert!(matches!(result.unwrap_err(), RelayClientError::Json(_))); } diff --git a/crates/sprout-mcp/src/server.rs b/crates/sprout-mcp/src/server.rs index 2a31fecb1..0d847a89d 100644 --- a/crates/sprout-mcp/src/server.rs +++ b/crates/sprout-mcp/src/server.rs @@ -239,12 +239,10 @@ impl SproutMcpServer { description = "Send a message to a Sprout channel" )] pub async fn send_message(&self, Parameters(p): Parameters) -> String { - // Validate channel_id is a well-formed UUID at the tool boundary. if let Err(e) = validate_uuid(&p.channel_id) { return format!("Error: {e}"); } - // Guard against excessively large message content. if p.content.len() > MAX_CONTENT_BYTES { return format!( "Error: content exceeds maximum size of {} bytes (got {})", diff --git a/crates/sprout-proxy/src/invite.rs b/crates/sprout-proxy/src/invite.rs index b14dff1b1..1fb17061d 100644 --- a/crates/sprout-proxy/src/invite.rs +++ b/crates/sprout-proxy/src/invite.rs @@ -74,7 +74,6 @@ mod tests { #[test] fn test_invite_token_validation() { - // Valid token let token = InviteToken::new("tok-valid", vec![], future(3600), 5); assert!(token.validate(Utc::now()).is_ok()); assert!(token.is_valid(Utc::now())); @@ -108,7 +107,6 @@ mod tests { // Still valid (uses < max_uses) assert!(token.is_valid(Utc::now())); token.consume(); - // Now exhausted assert!(!token.is_valid(Utc::now())); } diff --git a/crates/sprout-relay/src/api/agents.rs b/crates/sprout-relay/src/api/agents.rs index bf4095394..04a26a55c 100644 --- a/crates/sprout-relay/src/api/agents.rs +++ b/crates/sprout-relay/src/api/agents.rs @@ -42,7 +42,6 @@ pub async fn agents_handler( .await .map_err(|e| internal_error(&format!("db error: {e}")))?; - // Collect pubkeys for bulk presence lookup. let mut pubkeys_for_presence: Vec = Vec::new(); let mut bot_pubkey_hexes: Vec = Vec::new(); @@ -64,7 +63,6 @@ pub async fn agents_handler( Default::default() }); - // Fetch user records for name resolution. let user_records = state .db .get_users_bulk(&bots.iter().map(|b| b.pubkey.clone()).collect::>()) @@ -85,7 +83,6 @@ pub async fn agents_handler( let mut result = Vec::with_capacity(bots.len()); for (bot, hex) in bots.iter().zip(bot_pubkey_hexes.iter()) { - // Resolve display name: users table → bot record → test mapping → fallback. let name = user_name_map .get(hex.as_str()) .cloned() @@ -95,7 +92,6 @@ pub async fn agents_handler( format!("agent-{}", &hex[..end]) }); - // Parse channel names from comma-separated string, filtered to requester's access. let channels: Vec<&str> = bot .channel_names .split(',') @@ -103,7 +99,6 @@ pub async fn agents_handler( .filter(|s| !s.is_empty() && accessible_names.contains(*s)) .collect(); - // Parse capabilities from JSON value. let capabilities: Vec = bot .capabilities .as_ref() @@ -115,7 +110,6 @@ pub async fn agents_handler( }) .unwrap_or_default(); - // Presence status. let status = presence_map .get(hex.as_str()) .map(|s| s.as_str()) diff --git a/crates/sprout-relay/src/api/channels.rs b/crates/sprout-relay/src/api/channels.rs index 0e3bd01fc..a217eb49b 100644 --- a/crates/sprout-relay/src/api/channels.rs +++ b/crates/sprout-relay/src/api/channels.rs @@ -151,7 +151,6 @@ async fn resolve_dm_participants( let member_pubkeys: Vec> = members.iter().map(|m| m.pubkey.clone()).collect(); - // Bulk-fetch user records for name resolution. let user_records = state .db .get_users_bulk(&member_pubkeys) diff --git a/crates/sprout-relay/src/api/feed.rs b/crates/sprout-relay/src/api/feed.rs index c2ed7e5c7..2630e2a7a 100644 --- a/crates/sprout-relay/src/api/feed.rs +++ b/crates/sprout-relay/src/api/feed.rs @@ -62,14 +62,12 @@ pub async fn feed_handler( .and_then(|ts| DateTime::from_timestamp(ts, 0)) .unwrap_or_else(|| Utc::now() - Duration::days(7)); - // Parse optional type filter. let type_filter: Option> = params .types .as_deref() .map(|t| t.split(',').map(|s| s.trim()).collect()); let wants = |cat: &str| -> bool { type_filter.as_ref().is_none_or(|f| f.contains(cat)) }; - // 1. Get accessible channel IDs for this user. let accessible_ids = state .db .get_accessible_channel_ids(&pubkey_bytes) @@ -93,7 +91,6 @@ pub async fn feed_handler( }))); } - // 2. Run queries in parallel. let (mentions_res, needs_action_res, activity_res) = tokio::join!( state .db @@ -111,12 +108,10 @@ pub async fn feed_handler( let needs_action = needs_action_res.map_err(|e| internal_error(&format!("db error: {e}")))?; let activity_all = activity_res.map_err(|e| internal_error(&format!("db error: {e}")))?; - // 3. Partition activity into agent activity vs channel activity. let (agent_activity, channel_activity): (Vec<_>, Vec<_>) = activity_all .into_iter() .partition(|e| AGENT_KINDS.contains(&event_kind_u32(&e.event))); - // 4. Enrich events with channel names (batch lookup). let all_channels = state.db.list_channels(None).await.unwrap_or_else(|e| { tracing::warn!("feed: failed to load channel names for enrichment: {e}"); vec![] @@ -124,7 +119,6 @@ pub async fn feed_handler( let channel_name_map: HashMap = all_channels.into_iter().map(|c| (c.id, c.name)).collect(); - // Helper: convert a StoredEvent to a FeedItem JSON value. let to_feed_item = |event: &sprout_core::StoredEvent, category: &str| -> serde_json::Value { let channel_name = event .channel_id @@ -155,7 +149,6 @@ pub async fn feed_handler( }) }; - // 5. Build feed sections (apply type filter). let mentions_items: Vec = if wants("mentions") { mentions .iter() diff --git a/crates/sprout-relay/src/api/mod.rs b/crates/sprout-relay/src/api/mod.rs index 1fe96bb4e..0767c5f36 100644 --- a/crates/sprout-relay/src/api/mod.rs +++ b/crates/sprout-relay/src/api/mod.rs @@ -201,7 +201,6 @@ pub(crate) async fn check_channel_access( if is_member { return Ok(()); } - // Not an explicit member — check if channel is open. let is_open = state .db .get_channel(channel_id) diff --git a/crates/sprout-relay/src/api/presence.rs b/crates/sprout-relay/src/api/presence.rs index 9aa8de13d..ad836f00d 100644 --- a/crates/sprout-relay/src/api/presence.rs +++ b/crates/sprout-relay/src/api/presence.rs @@ -52,8 +52,6 @@ pub async fn presence_handler( .await .unwrap_or_default(); - // Build result: pubkey_hex → status. Include "offline" for any requested - // pubkey not found in the presence map. let mut result = serde_json::Map::new(); for pk in &pubkeys { let hex = pk.to_hex(); diff --git a/crates/sprout-relay/src/api/search.rs b/crates/sprout-relay/src/api/search.rs index 9b9c5baeb..d0050f065 100644 --- a/crates/sprout-relay/src/api/search.rs +++ b/crates/sprout-relay/src/api/search.rs @@ -39,7 +39,6 @@ pub async fn search_handler( let query_str = params.q.unwrap_or_default(); let per_page = params.limit.unwrap_or(20).min(100); - // Get accessible channel IDs to scope the search. let channel_ids = state .db .get_accessible_channel_ids(&pubkey_bytes) @@ -74,7 +73,6 @@ pub async fn search_handler( } }; - // Enrich hits with channel names. let all_channels = state.db.list_channels(None).await.unwrap_or_default(); let channel_name_map: HashMap = all_channels .into_iter() diff --git a/crates/sprout-relay/src/handlers/event.rs b/crates/sprout-relay/src/handlers/event.rs index be306fc96..c4b49926d 100644 --- a/crates/sprout-relay/src/handlers/event.rs +++ b/crates/sprout-relay/src/handlers/event.rs @@ -449,13 +449,11 @@ async fn derive_reaction_channel( None => return ReactionChannelResult::NoTarget, }; - // Decode hex to bytes for DB lookup (already validated as 64-char hex above) let id_bytes = match hex::decode(&target_hex) { Ok(b) if b.len() == 32 => b, _ => return ReactionChannelResult::NoTarget, }; - // Look up the target event to get its channel_id match db.get_event_by_id(&id_bytes).await { Ok(Some(target_event)) => { if let Some(ch_id) = target_event.channel_id { diff --git a/crates/sprout-relay/src/handlers/req.rs b/crates/sprout-relay/src/handlers/req.rs index 7af353e31..99335320e 100644 --- a/crates/sprout-relay/src/handlers/req.rs +++ b/crates/sprout-relay/src/handlers/req.rs @@ -121,7 +121,6 @@ pub async fn handle_req( }; for stored in &events { - // Deduplicate across filters by event ID. if !seen_ids.insert(stored.event.id) { continue; } @@ -255,7 +254,6 @@ mod tests { fn test_extract_channel_id_mixed_channels_returns_none() { let channel_a = uuid::Uuid::new_v4(); let channel_b = uuid::Uuid::new_v4(); - // Two filters each with a different channel ID — can't index under one channel. let filters = vec![ filter_with_channel(channel_a), filter_with_channel(channel_b), @@ -265,7 +263,6 @@ mod tests { #[test] fn test_extract_channel_id_no_channel_tag_returns_none() { - // A filter with no channel tag means "global subscription". let filters = vec![Filter::new()]; assert_eq!(extract_channel_id_from_filters(&filters), None); } @@ -274,16 +271,12 @@ mod tests { fn test_extract_channel_id_one_filter_missing_channel_returns_none() { // Even if one filter has a channel, a second filter without one makes it global. let channel_id = uuid::Uuid::new_v4(); - let filters = vec![ - filter_with_channel(channel_id), - Filter::new(), // no channel tag → global - ]; + let filters = vec![filter_with_channel(channel_id), Filter::new()]; assert_eq!(extract_channel_id_from_filters(&filters), None); } #[test] fn test_extract_channel_id_same_channel_multiple_filters() { - // Two filters both scoped to the same channel → returns that channel. let channel_id = uuid::Uuid::new_v4(); let filters = vec![ filter_with_channel(channel_id), diff --git a/crates/sprout-relay/src/main.rs b/crates/sprout-relay/src/main.rs index 854a0e16c..d72cc1a0b 100644 --- a/crates/sprout-relay/src/main.rs +++ b/crates/sprout-relay/src/main.rs @@ -87,7 +87,6 @@ async fn main() -> anyhow::Result<()> { let workflow_config = sprout_workflow::WorkflowConfig::default(); let workflow_engine = Arc::new(WorkflowEngine::new(db.clone(), workflow_config)); - // Spawn cron scheduler background task. let wf_cron = Arc::clone(&workflow_engine); tokio::spawn(async move { wf_cron.run().await }); diff --git a/crates/sprout-relay/src/protocol.rs b/crates/sprout-relay/src/protocol.rs index 2a3576c32..17478f628 100644 --- a/crates/sprout-relay/src/protocol.rs +++ b/crates/sprout-relay/src/protocol.rs @@ -290,7 +290,6 @@ mod tests { fn parse_req_too_many_filters_is_rejected() { let filter = Filter::new().kind(Kind::TextNote); let filter_val = serde_json::to_value(&filter).unwrap(); - // Build a REQ with MAX_FILTERS_PER_REQ + 1 filters. let mut arr: Vec = vec![ serde_json::Value::String("REQ".to_string()), serde_json::Value::String("sub3".to_string()), diff --git a/crates/sprout-relay/src/router.rs b/crates/sprout-relay/src/router.rs index d949f5050..63a6f61a5 100644 --- a/crates/sprout-relay/src/router.rs +++ b/crates/sprout-relay/src/router.rs @@ -77,7 +77,6 @@ async fn nip11_or_ws_handler( return Json(info).into_response(); } - // Try WebSocket upgrade from the raw request. match WebSocketUpgrade::from_request(req, &state).await { Ok(ws) => ws .on_upgrade(move |socket| handle_connection(socket, state, addr)) diff --git a/crates/sprout-relay/src/subscription.rs b/crates/sprout-relay/src/subscription.rs index 3d7870910..3f0cfa6a9 100644 --- a/crates/sprout-relay/src/subscription.rs +++ b/crates/sprout-relay/src/subscription.rs @@ -606,7 +606,6 @@ mod tests { let conn_id = Uuid::new_v4(); let channel_id = Uuid::new_v4(); - // Build a filter with an explicit empty kinds list. let filter_empty_kinds = Filter::new().kinds(vec![] as Vec); registry.register( conn_id, @@ -615,13 +614,11 @@ mod tests { Some(channel_id), ); - // The subscription should not appear in the wildcard index. assert!( registry.channel_wildcard_index.get(&channel_id).is_none(), "kinds:[] sub must NOT be in the wildcard index" ); - // The subscription should not appear in any kind-specific index. let key = IndexKey { channel_id, kind: Kind::TextNote, @@ -631,7 +628,6 @@ mod tests { "kinds:[] sub must NOT be in the kind-specific index" ); - // Fan-out should produce zero matches for any event kind. let event = make_stored_event(Kind::TextNote, Some(channel_id)); let matches = registry.fan_out(&event); assert!( @@ -657,7 +653,6 @@ mod tests { let conn_id = Uuid::new_v4(); let channel_id = Uuid::new_v4(); - // Register a global (channel-less) subscription that matches all TextNote events. registry.register( conn_id, "global_sub".to_string(), @@ -665,7 +660,6 @@ mod tests { None, // global — no channel scope ); - // A channel-scoped event must NOT be delivered to the global subscription. let channel_event = make_stored_event(Kind::TextNote, Some(channel_id)); let matches = registry.fan_out(&channel_event); assert!( @@ -674,7 +668,6 @@ mod tests { matches ); - // A non-channel event SHOULD still be delivered to the global subscription. let global_event = make_stored_event(Kind::TextNote, None); let matches = registry.fan_out(&global_event); assert_eq!( @@ -700,10 +693,8 @@ mod tests { Some(channel_id), ); - // Should not panic. registry.remove_subscription(conn_id, "sub_empty"); - // Indexes remain clean. assert!(registry.channel_wildcard_index.get(&channel_id).is_none()); let key = IndexKey { channel_id, diff --git a/crates/sprout-search/src/collection.rs b/crates/sprout-search/src/collection.rs index 55e1d51e1..8f7dbc5e4 100644 --- a/crates/sprout-search/src/collection.rs +++ b/crates/sprout-search/src/collection.rs @@ -29,7 +29,6 @@ pub async fn ensure_collection( api_key: &str, collection_name: &str, ) -> Result<(), SearchError> { - // First, check if the collection already exists. let check_url = format!("{}/collections/{}", base_url, collection_name); let resp = client .get(&check_url) @@ -43,7 +42,6 @@ pub async fn ensure_collection( return Ok(()); } 404 => { - // Collection doesn't exist — create it. debug!( collection = collection_name, "Collection not found, creating" @@ -55,7 +53,6 @@ pub async fn ensure_collection( } } - // Create the collection. let schema = events_schema(collection_name); let create_url = format!("{}/collections", base_url); let resp = client diff --git a/crates/sprout-test-client/src/lib.rs b/crates/sprout-test-client/src/lib.rs index 67fa5b735..19fe4bca3 100644 --- a/crates/sprout-test-client/src/lib.rs +++ b/crates/sprout-test-client/src/lib.rs @@ -14,7 +14,6 @@ use tokio::time::timeout; use tokio_tungstenite::{connect_async, tungstenite::Message, MaybeTlsStream, WebSocketStream}; use tracing::debug; -// Re-export shared relay wire types from sprout-mcp. pub use sprout_mcp::relay_client::{parse_relay_message, OkResponse, RelayMessage}; /// Errors returned by [`SproutTestClient`] operations. diff --git a/crates/sprout-test-client/tests/e2e_mcp.rs b/crates/sprout-test-client/tests/e2e_mcp.rs index 29470b92c..72a224b7b 100644 --- a/crates/sprout-test-client/tests/e2e_mcp.rs +++ b/crates/sprout-test-client/tests/e2e_mcp.rs @@ -134,7 +134,6 @@ impl McpSession { continue; } - // Check this is our response. if v["id"] == json!(id) { return v; } @@ -416,12 +415,10 @@ async fn test_mcp_send_and_read_message() { "get_channel_history returned an error: {history_text}" ); - // The history should be a JSON array of events. let events: Vec = serde_json::from_str(&history_text).unwrap_or_else(|e| { panic!("get_channel_history response is not valid JSON array: {e}\n{history_text}") }); - // Find our message in the history. let found = events .iter() .any(|ev| ev["content"].as_str().unwrap_or("").contains(&unique_token)); @@ -566,7 +563,6 @@ async fn test_mcp_create_and_trigger_workflow() { return; } - // Parse the created workflow to get its ID. let workflow: Value = serde_json::from_str(&create_text).unwrap_or_else(|e| { panic!("create_workflow response is not valid JSON: {e}\n{create_text}") }); @@ -577,7 +573,6 @@ async fn test_mcp_create_and_trigger_workflow() { assert!(!workflow_id.is_empty(), "workflow id must not be empty"); - // Verify the workflow name matches. assert_eq!( workflow["name"].as_str().unwrap_or(""), workflow_name, @@ -679,7 +674,6 @@ async fn test_mcp_create_and_trigger_workflow() { "expected at least one run after triggering workflow '{workflow_id}'" ); - // Verify our run is in the list. let found_run = runs.iter().any(|r| r["id"].as_str() == Some(run_id)); assert!( found_run, diff --git a/crates/sprout-test-client/tests/e2e_relay.rs b/crates/sprout-test-client/tests/e2e_relay.rs index f346a5b9a..de73e0ad3 100644 --- a/crates/sprout-test-client/tests/e2e_relay.rs +++ b/crates/sprout-test-client/tests/e2e_relay.rs @@ -205,7 +205,6 @@ async fn test_close_subscription_stops_delivery() { .await .expect("EOSE"); - // Close the subscription. client .close_subscription(&sid) .await @@ -302,7 +301,6 @@ async fn test_multiple_concurrent_clients() { .expect("EOSE"); } - // Client 0 sends the event. let content = format!("broadcast-{}", uuid::Uuid::new_v4()); let ok = clients[0] .send_text_message(&keys[0], &channel, &content, kind) @@ -342,7 +340,6 @@ async fn test_stored_events_returned_before_eose() { .await .expect("connect"); - // Send an event first. let content = format!("stored-{}", uuid::Uuid::new_v4()); let ok = client .send_text_message(&keys, &channel, &content, kind) diff --git a/crates/sprout-test-client/tests/e2e_rest_api.rs b/crates/sprout-test-client/tests/e2e_rest_api.rs index 6b1e3a995..e401edb33 100644 --- a/crates/sprout-test-client/tests/e2e_rest_api.rs +++ b/crates/sprout-test-client/tests/e2e_rest_api.rs @@ -111,7 +111,6 @@ async fn test_list_channels_returns_expected_fields() { "expected at least one channel in the list" ); - // Every channel must have the required fields. for ch in channels { assert!(ch.get("id").is_some(), "channel missing 'id' field"); assert!(ch.get("name").is_some(), "channel missing 'name' field"); @@ -269,7 +268,6 @@ async fn test_search_returns_results_for_open_channels() { let hits = body["hits"].as_array().expect("'hits' must be an array"); - // Every hit must have the required fields. for hit in hits { assert!(hit.get("event_id").is_some(), "hit missing 'event_id'"); assert!(hit.get("content").is_some(), "hit missing 'content'"); @@ -288,11 +286,9 @@ async fn test_search_returns_indexed_event() { let pubkey_hex = keys.public_key().to_hex(); let ws_url = relay_ws_url(); - // Send a message with a unique token via WebSocket so it gets indexed. let unique_token = format!("e2e-search-{}", uuid::Uuid::new_v4().simple()); let content = format!("E2E REST search test marker: {unique_token}"); - // Connect and send the message to an open channel. let mut ws_client = SproutTestClient::connect(&ws_url, &keys) .await .expect("WebSocket connect failed"); @@ -313,7 +309,6 @@ async fn test_search_returns_indexed_event() { // Wait briefly for the search index to catch up. tokio::time::sleep(Duration::from_millis(500)).await; - // Search for the unique token. // The unique_token is UUID simple format (hex only) — safe to use directly in the URL. let url = format!("{}/api/search?q={unique_token}", relay_http_url()); let resp = authed_get(&client, &url, &pubkey_hex).await; @@ -328,7 +323,6 @@ async fn test_search_returns_indexed_event() { "expected at least one search hit for unique token '{unique_token}'" ); - // The first hit should contain our unique token. let first_content = hits[0]["content"].as_str().unwrap_or(""); assert!( first_content.contains(&unique_token), @@ -383,7 +377,6 @@ async fn test_presence_set_and_query() { let pubkey_hex = keys.public_key().to_hex(); let ws_url = relay_ws_url(); - // Send a presence event via WebSocket. let mut ws_client = SproutTestClient::connect(&ws_url, &keys) .await .expect("WebSocket connect failed"); @@ -401,7 +394,6 @@ async fn test_presence_set_and_query() { // Keep the WebSocket connection alive briefly so presence is registered. tokio::time::sleep(Duration::from_millis(200)).await; - // Query presence via REST. let url = format!("{}/api/presence?pubkeys={pubkey_hex}", relay_http_url()); let resp = authed_get(&client, &url, &pubkey_hex).await; @@ -414,7 +406,6 @@ async fn test_presence_set_and_query() { "expected 'online' after sending presence event" ); - // Clean up: send offline presence. let offline_event = nostr::EventBuilder::new(Kind::Custom(20001), "offline", []) .sign_with_keys(&keys) .expect("event sign failed"); @@ -444,7 +435,6 @@ async fn test_presence_bulk_query() { let body: serde_json::Value = resp.json().await.expect("JSON"); assert!(body.is_object(), "presence response must be an object"); - // Both pubkeys should appear in the response. assert!( body.get(&pk_a).is_some(), "pk_a missing from presence response" @@ -454,7 +444,6 @@ async fn test_presence_bulk_query() { "pk_b missing from presence response" ); - // Both should be offline. assert_eq!(body[&pk_a].as_str(), Some("offline")); assert_eq!(body[&pk_b].as_str(), Some("offline")); } @@ -499,7 +488,6 @@ async fn test_agents_list() { .as_array() .expect("/api/agents must return a JSON array"); - // Every agent must have the required fields. for agent in agents { assert!(agent.get("pubkey").is_some(), "agent missing 'pubkey'"); assert!(agent.get("name").is_some(), "agent missing 'name'"); @@ -559,7 +547,6 @@ async fn test_agents_scoped_to_accessible_channels() { let agents: Vec = resp.json().await.expect("JSON"); - // Get accessible channels for this user. let channels_url = format!("{}/api/channels", relay_http_url()); let channels_resp = authed_get(&client, &channels_url, &pubkey_hex).await; let channels: Vec = channels_resp.json().await.expect("JSON"); @@ -632,17 +619,14 @@ async fn test_feed_returns_activity() { // Small delay to let the event propagate. tokio::time::sleep(Duration::from_millis(200)).await; - // Fetch the feed. let resp = authed_get(&client, &url, &pubkey_hex).await; assert_eq!(resp.status(), 200, "expected 200 OK from /api/feed"); let body: serde_json::Value = resp.json().await.expect("response must be JSON"); - // Top-level structure. let feed = body.get("feed").expect("response missing 'feed' key"); let meta = body.get("meta").expect("response missing 'meta' key"); - // Feed sections must exist. assert!(feed.get("mentions").is_some(), "feed missing 'mentions'"); assert!( feed.get("needs_action").is_some(), @@ -654,7 +638,6 @@ async fn test_feed_returns_activity() { "feed missing 'agent_activity'" ); - // Meta fields. assert!(meta.get("since").is_some(), "meta missing 'since'"); assert!(meta.get("total").is_some(), "meta missing 'total'"); assert!( @@ -662,7 +645,6 @@ async fn test_feed_returns_activity() { "meta missing 'generated_at'" ); - // Activity must be an array. assert!( feed["activity"].is_array(), "feed 'activity' must be an array" diff --git a/crates/sprout-test-client/tests/e2e_workflows.rs b/crates/sprout-test-client/tests/e2e_workflows.rs index 5d61eb956..2ca78b311 100644 --- a/crates/sprout-test-client/tests/e2e_workflows.rs +++ b/crates/sprout-test-client/tests/e2e_workflows.rs @@ -157,7 +157,6 @@ async fn test_list_workflows_empty_channel() { let body: serde_json::Value = resp.json().await.expect("response must be JSON"); assert!(body.is_array(), "expected JSON array, got: {body}"); - // Every workflow in the list must have required fields. let arr = body.as_array().unwrap(); for wf in arr { assert!(wf.get("id").is_some(), "workflow missing 'id' field"); @@ -180,7 +179,6 @@ async fn test_create_and_list_workflow() { let yaml = webhook_workflow_yaml("e2e-create-list-test"); let created = create_workflow(&client, &base, pubkey_hex, CHANNEL_GENERAL, &yaml).await; - // Response must include id, name, channel_id, definition fields. let workflow_id = created["id"] .as_str() .expect("created workflow must have 'id'"); @@ -199,7 +197,6 @@ async fn test_create_and_list_workflow() { "webhook workflow must return 'webhook_secret' on creation" ); - // Verify it appears in the list. let list_url = format!("{base}/api/channels/{CHANNEL_GENERAL}/workflows"); let list_resp = client .get(&list_url) @@ -216,7 +213,6 @@ async fn test_create_and_list_workflow() { "newly created workflow {workflow_id} not found in list" ); - // Clean up. let status = delete_workflow(&client, &base, pubkey_hex, workflow_id).await; assert_eq!(status, 204, "cleanup DELETE should return 204"); } @@ -235,7 +231,6 @@ async fn test_trigger_workflow_and_check_run() { let pubkey_hex: &str = SEEDED_PUBKEY; let base = relay_http_url(); - // Create a webhook workflow. let yaml = webhook_workflow_yaml("e2e-trigger-test"); let created = create_workflow(&client, &base, pubkey_hex, CHANNEL_GENERAL, &yaml).await; let workflow_id = created["id"] @@ -243,7 +238,6 @@ async fn test_trigger_workflow_and_check_run() { .expect("workflow must have 'id'") .to_string(); - // Manually trigger the workflow via POST /api/workflows/:id/trigger. let trigger_url = format!("{base}/api/workflows/{workflow_id}/trigger"); let trigger_resp = client .post(&trigger_url) @@ -297,7 +291,6 @@ async fn test_trigger_workflow_and_check_run() { let run = found_run.expect("run must appear in GET /api/workflows/:id/runs within 1 second"); - // Run must have required fields. assert!(run.get("id").is_some(), "run missing 'id'"); assert!( run.get("workflow_id").is_some(), @@ -305,14 +298,12 @@ async fn test_trigger_workflow_and_check_run() { ); assert!(run.get("status").is_some(), "run missing 'status'"); - // Status must be one of the valid terminal or in-progress values. let status = run["status"].as_str().unwrap_or(""); assert!( matches!(status, "pending" | "running" | "completed" | "failed"), "run status '{status}' is not a recognized value" ); - // Clean up. let del_status = delete_workflow(&client, &base, pubkey_hex, &workflow_id).await; assert_eq!(del_status, 204, "cleanup DELETE should return 204"); } @@ -402,7 +393,6 @@ steps: "run status '{status}' is not a recognized value" ); - // Clean up. let _ = ws_client.disconnect().await; let del_status = delete_workflow(&client, &base, pubkey_hex, &workflow_id).await; assert_eq!(del_status, 204, "cleanup DELETE should return 204"); @@ -520,7 +510,6 @@ steps: "run status '{status}' is not a recognized value" ); - // Clean up. let _ = ws_client.disconnect().await; let del_status = delete_workflow(&client, &base, pubkey_hex, &workflow_id).await; assert_eq!(del_status, 204, "cleanup DELETE should return 204"); diff --git a/crates/sprout-workflow/src/executor.rs b/crates/sprout-workflow/src/executor.rs index 720b03d37..7bfd106f9 100644 --- a/crates/sprout-workflow/src/executor.rs +++ b/crates/sprout-workflow/src/executor.rs @@ -73,7 +73,6 @@ pub fn resolve_template( trigger_ctx: &TriggerContext, step_outputs: &HashMap, ) -> Result { - // Fast path: no template markers. if !template.contains("{{") { return Ok(template.to_owned()); } @@ -82,11 +81,9 @@ pub fn resolve_template( let mut remaining = template; while let Some(start) = remaining.find("{{") { - // Append everything before the `{{`. result.push_str(&remaining[..start]); remaining = &remaining[start + 2..]; - // Find the closing `}}`. let end = match remaining.find("}}") { Some(e) => e, None => { @@ -105,10 +102,8 @@ pub fn resolve_template( let var_path = parts.next().unwrap_or("").trim(); let filter = parts.next().map(|s| s.trim()); - // Resolve the variable. let raw_value = resolve_variable(var_path, trigger_ctx, step_outputs); - // Apply filter (if any). let value = match (raw_value, filter) { (Some(v), Some(f)) => apply_filter(v, f)?, (Some(v), None) => v, @@ -124,7 +119,6 @@ pub fn resolve_template( result.push_str(&value); } - // Append any trailing text after the last `}}`. result.push_str(remaining); Ok(result) } @@ -141,7 +135,6 @@ fn resolve_variable( // Pattern: `steps.STEP_ID.output.FIELD` if let Some(rest) = path.strip_prefix("steps.") { - // rest = "STEP_ID.output.FIELD" let mut parts = rest.splitn(3, '.'); let step_id = parts.next()?; let middle = parts.next()?; // must be "output" @@ -184,7 +177,6 @@ fn json_to_string(v: &JsonValue) -> String { fn apply_filter(value: String, filter: &str) -> Result { let filter = filter.trim(); - // `truncate(N)` — truncate to N characters. if let Some(inner) = filter .strip_prefix("truncate(") .and_then(|s| s.strip_suffix(')')) @@ -331,7 +323,6 @@ pub fn build_eval_context( } // ── Step outputs ────────────────────────────────────────────────────────── - // Register as `steps_STEP_ID_output_FIELD`. for (step_id, output) in step_outputs { if let JsonValue::Object(map) = output { @@ -589,7 +580,6 @@ pub async fn dispatch_action( "RequestApproval from={from} timeout={timeout_str}: {message}" ); - // Generate an approval token. let token = generate_approval_token(run_id, step_id); // TODO (WF-08): create approval record in DB, emit kind:46010. @@ -723,7 +713,6 @@ async fn call_webhook_impl( use std::time::Duration; // ── SSRF check ──────────────────────────────────────────────────────────── - // Parse the URL to extract host and port before making any connection. let parsed_url = reqwest::Url::parse(url) .map_err(|e| WorkflowError::WebhookError(format!("invalid URL: {e}")))?; @@ -857,7 +846,6 @@ pub async fn execute_run( ) })?; - // Mark run as Running now that we have a permit. engine .db .update_workflow_run( @@ -969,7 +957,6 @@ async fn execute_steps( continue; } - // 1. Evaluate `if:` condition. if let Some(expr) = &step.if_expr { match evaluate_condition(expr, trigger_ctx, &step_outputs).await { Ok(true) => { @@ -994,7 +981,6 @@ async fn execute_steps( } } - // 2. Resolve template variables. let resolved_action = match resolve_step_templates(step, trigger_ctx, &step_outputs) { Ok(a) => a, Err(e) => { @@ -1006,7 +992,6 @@ async fn execute_steps( } }; - // 3. Dispatch action (with per-step timeout). let timeout_secs = step .timeout_secs .unwrap_or(engine.config.default_timeout_secs); @@ -1139,8 +1124,7 @@ mod tests { let ctx = make_trigger(); let out = resolve_template("{{trigger.text | truncate(5)}}", &ctx, &HashMap::new()).unwrap(); - assert_eq!(out, "P1 in"); // "P1 incident in production" truncated to 5 chars = "P1 in" - // Actually "P1 in" is 5 chars: 'P','1',' ','i','n' + assert_eq!(out, "P1 in"); assert_eq!(out.chars().count(), 5); } @@ -1153,7 +1137,6 @@ mod tests { &HashMap::new(), ) .unwrap(); - // "abc123def456" → "abc123...def456" assert_eq!(out, "abc123...def456"); } diff --git a/crates/sprout-workflow/src/lib.rs b/crates/sprout-workflow/src/lib.rs index 2192a919b..ff212548d 100644 --- a/crates/sprout-workflow/src/lib.rs +++ b/crates/sprout-workflow/src/lib.rs @@ -249,7 +249,6 @@ impl WorkflowEngine { continue; } - // Create the workflow_run row (status: pending). let trigger_event_id_bytes = event.event.id.as_bytes().to_vec(); let run_id = match self .db @@ -314,7 +313,6 @@ async fn should_fire_workflow( trigger_ctx: &executor::TriggerContext, workflow_id: uuid::Uuid, ) -> bool { - // Enforce reaction emoji filter. if let TriggerDef::ReactionAdded { emoji: Some(ref expected), } = def.trigger @@ -330,7 +328,6 @@ async fn should_fire_workflow( } } - // Evaluate trigger filter expression (MessagePosted only). if let TriggerDef::MessagePosted { filter: Some(ref expr), } = def.trigger @@ -465,7 +462,6 @@ steps: let (def, json) = WorkflowEngine::parse_yaml(yaml).expect("parse failed"); assert_eq!(def.name, "Test Workflow"); - // JSON must round-trip. let reparsed: WorkflowDef = serde_json::from_str(&json).expect("json round-trip"); assert_eq!(reparsed.name, def.name); assert_eq!(reparsed.steps.len(), 1); diff --git a/crates/sprout-workflow/src/schema.rs b/crates/sprout-workflow/src/schema.rs index 73e8fb98a..ccf453165 100644 --- a/crates/sprout-workflow/src/schema.rs +++ b/crates/sprout-workflow/src/schema.rs @@ -175,7 +175,6 @@ impl WorkflowDef { && id.chars().all(|c| c.is_ascii_alphanumeric() || c == '_') }; - // Step IDs must be unique and non-empty. let mut seen_ids: HashSet<&str> = HashSet::new(); for step in &self.steps { if step.id.trim().is_empty() { @@ -197,28 +196,23 @@ impl WorkflowDef { } } - // Validate schedule trigger fields. if let TriggerDef::Schedule { cron, interval } = &self.trigger { - // Must have cron or interval (not neither). if cron.is_none() && interval.is_none() { return Err(WorkflowError::InvalidDefinition( "schedule trigger requires either 'cron' or 'interval'".into(), )); } - // Must not have both cron and interval simultaneously. if cron.is_some() && interval.is_some() { return Err(WorkflowError::InvalidDefinition( "schedule trigger cannot specify both 'cron' and 'interval'; use one or the other".into(), )); } - // Validate cron expression syntax. if let Some(expr) = cron { validate_cron(expr)?; } - // Validate interval format (e.g. "30m", "1h", "60s"). if let Some(dur) = interval { crate::executor::parse_duration_secs(dur).map_err(|_| { WorkflowError::InvalidDefinition(format!( @@ -290,7 +284,6 @@ mod tests { assert_eq!(def.steps.len(), 1); assert_eq!(def.steps[0].id, "notify"); - // Canonical JSON must round-trip. let reparsed: WorkflowDef = serde_json::from_str(&json).expect("json round-trip"); assert_eq!(reparsed.name, def.name); } @@ -360,7 +353,6 @@ mod tests { let (def, _) = parse_yaml(yaml).expect("parse failed"); assert_eq!(def.steps.len(), 7); - // Verify each action type deserialized correctly. assert!(matches!( &def.steps[0].action, ActionDef::SendMessage { .. } @@ -672,7 +664,6 @@ mod tests { ); let (def, json) = parse_yaml(yaml).expect("parse failed"); - // Round-trip through JSON. let reparsed: WorkflowDef = serde_json::from_str(&json).expect("json round-trip"); assert_eq!(reparsed.name, def.name);