diff --git a/examples/custom_channel.rs b/examples/custom_channel.rs index 790762d..30476b0 100644 --- a/examples/custom_channel.rs +++ b/examples/custom_channel.rs @@ -13,7 +13,7 @@ pub struct ChannelMessage { pub id: String, pub sender: String, /// Channel-specific reply address (e.g. Telegram chat_id, Discord channel_id). - pub reply_to: String, + pub reply_target: String, pub content: String, pub channel: String, pub timestamp: u64, @@ -97,7 +97,7 @@ impl Channel for TelegramChannel { let channel_msg = ChannelMessage { id: msg["message_id"].to_string(), sender, - reply_to: chat_id, + reply_target: chat_id, content: msg["text"].as_str().unwrap_or("").to_string(), channel: "telegram".into(), timestamp: msg["date"].as_u64().unwrap_or(0), diff --git a/src/channels/irc.rs b/src/channels/irc.rs index 2e03378..8bdd633 100644 --- a/src/channels/irc.rs +++ b/src/channels/irc.rs @@ -551,7 +551,7 @@ impl Channel for IrcChannel { // Determine reply target: if sent to a channel, reply to channel; // if DM (target == our nick), reply to sender let is_channel = target.starts_with('#') || target.starts_with('&'); - let reply_to = if is_channel { + let reply_target = if is_channel { target.to_string() } else { sender_nick.to_string() @@ -566,7 +566,7 @@ impl Channel for IrcChannel { let channel_msg = ChannelMessage { id: format!("irc_{}_{seq}", chrono::Utc::now().timestamp_millis()), sender: sender_nick.to_string(), - reply_target: reply_to, + reply_target, content, channel: "irc".to_string(), timestamp: std::time::SystemTime::now() diff --git a/tests/reply_target_field_regression.rs b/tests/reply_target_field_regression.rs new file mode 100644 index 0000000..e1d6358 --- /dev/null +++ b/tests/reply_target_field_regression.rs @@ -0,0 +1,70 @@ +//! Regression guard for ChannelMessage field naming consistency. +//! +//! This test prevents accidental reintroduction of the removed `reply_to` field +//! in Rust source code where `reply_target` must be used. + +use std::fs; +use std::path::{Path, PathBuf}; + +const SCAN_PATHS: &[&str] = &["src", "examples"]; +const FORBIDDEN_PATTERNS: &[&str] = &[".reply_to", "reply_to:"]; + +fn collect_rs_files(dir: &Path, out: &mut Vec) { + let entries = fs::read_dir(dir) + .unwrap_or_else(|err| panic!("Failed to read directory {}: {err}", dir.display())); + + for entry in entries { + let entry = + entry.unwrap_or_else(|err| panic!("Failed to read entry in {}: {err}", dir.display())); + let path = entry.path(); + + if path.is_dir() { + collect_rs_files(&path, out); + } else if path.extension().is_some_and(|ext| ext == "rs") { + out.push(path); + } + } +} + +#[test] +fn source_does_not_use_legacy_reply_to_field() { + let root = Path::new(env!("CARGO_MANIFEST_DIR")); + let mut rust_files = Vec::new(); + + for relative in SCAN_PATHS { + collect_rs_files(&root.join(relative), &mut rust_files); + } + + rust_files.sort(); + + let mut violations = Vec::new(); + + for file_path in rust_files { + let content = fs::read_to_string(&file_path).unwrap_or_else(|err| { + panic!("Failed to read source file {}: {err}", file_path.display()) + }); + + for (line_idx, line) in content.lines().enumerate() { + for pattern in FORBIDDEN_PATTERNS { + if line.contains(pattern) { + let rel = file_path + .strip_prefix(root) + .unwrap_or(&file_path) + .display() + .to_string(); + violations.push(format!( + "{rel}:{} contains forbidden pattern `{pattern}`: {}", + line_idx + 1, + line.trim() + )); + } + } + } + } + + assert!( + violations.is_empty(), + "Found legacy `reply_to` field usage:\n{}", + violations.join("\n") + ); +}