#[cfg(test)]
mod tests {
use anyhow::Result;
use async_trait::async_trait;
use sea_orm::DatabaseConnection;
use std::sync::Arc;
use crate::plugin::{MessageEvent, SensoriumLoop, Worker};
struct EchoWorker;
#[async_trait]
impl Worker for EchoWorker {
fn name(&self) -> &'static str {
"echo"
}
fn description(&self) -> &'static str {
"echo worker for tests"
}
fn example(&self) -> &'static str {
"/echo"
}
async fn handle(
&self,
_db: DatabaseConnection,
_msg: MessageEvent,
_args: std::collections::HashMap<String, String>,
) -> Result<bool> {
Ok(true)
}
}
#[test]
fn register_worker_accepts_valid_pattern() {
let mut sloop = SensoriumLoop::new();
let result = sloop.register_worker(r"^hello", Arc::new(EchoWorker));
assert!(result.is_ok(), "valid pattern should register successfully");
}
#[test]
fn register_worker_rejects_duplicate_pattern() {
let mut sloop = SensoriumLoop::new();
sloop
.register_worker(r"^hello", Arc::new(EchoWorker))
.unwrap();
let result = sloop.register_worker(r"^hello", Arc::new(EchoWorker));
assert!(result.is_err(), "duplicate pattern should return Err");
}
#[test]
fn register_worker_rejects_invalid_regex() {
let mut sloop = SensoriumLoop::new();
let result = sloop.register_worker(r"[invalid", Arc::new(EchoWorker));
assert!(result.is_err(), "invalid regex pattern should return Err");
}
#[test]
fn register_worker_allows_different_patterns() {
let mut sloop = SensoriumLoop::new();
sloop
.register_worker(r"^hello", Arc::new(EchoWorker))
.unwrap();
let result = sloop.register_worker(r"^world", Arc::new(EchoWorker));
assert!(result.is_ok(), "different patterns should both register");
}
#[test]
fn register_worker_rejects_pattern_matching_help() {
let mut sloop = SensoriumLoop::new();
let result = sloop.register_worker(r"(?i)/help", Arc::new(EchoWorker));
assert!(result.is_err(), "pattern matching /help should be rejected");
let err = result.unwrap_err();
assert!(
err.to_string().contains("reserved"),
"error should mention reserved: {err}"
);
}
#[test]
fn register_worker_rejects_uppercase_help_pattern() {
let mut sloop = SensoriumLoop::new();
let result = sloop.register_worker(r"^/HELP$", Arc::new(EchoWorker));
assert!(
result.is_err(),
"uppercase /HELP pattern should be rejected"
);
}
use std::sync::atomic::{AtomicUsize, Ordering};
struct CountingWorker {
pub name: &'static str,
pub call_count: Arc<AtomicUsize>,
}
#[async_trait]
impl Worker for CountingWorker {
fn name(&self) -> &'static str {
self.name
}
fn description(&self) -> &'static str {
"counting worker for tests"
}
fn example(&self) -> &'static str {
"/count"
}
async fn handle(
&self,
_db: DatabaseConnection,
_msg: MessageEvent,
_args: std::collections::HashMap<String, String>,
) -> Result<bool> {
self.call_count.fetch_add(1, Ordering::SeqCst);
Ok(true)
}
}
fn find_matching_worker<'a>(
workers: &'a [crate::plugin::WorkerRegistration],
content: &str,
) -> Option<&'a crate::plugin::WorkerRegistration> {
workers.iter().find(|r| r.pattern.is_match(content))
}
#[test]
fn dispatch_selects_first_matching_worker() {
let counter_a = Arc::new(AtomicUsize::new(0));
let counter_b = Arc::new(AtomicUsize::new(0));
let mut sloop = SensoriumLoop::new();
sloop
.register_worker(
r"^/deploy",
Arc::new(CountingWorker {
name: "deploy-worker",
call_count: counter_a.clone(),
}),
)
.unwrap();
sloop
.register_worker(
r"^/dev",
Arc::new(CountingWorker {
name: "generic-worker",
call_count: counter_b.clone(),
}),
)
.unwrap();
let workers = sloop.workers();
let matched = find_matching_worker(workers, "/deploy now");
assert!(matched.is_some(), "should find a match for '/deploy now'");
assert_eq!(
matched.unwrap().worker.name(),
"deploy-worker",
"first matching worker should be deploy-worker"
);
}
#[test]
fn dispatch_returns_none_for_no_match() {
let counter = Arc::new(AtomicUsize::new(0));
let mut sloop = SensoriumLoop::new();
sloop
.register_worker(
r"^/deploy",
Arc::new(CountingWorker {
name: "deploy-worker",
call_count: counter.clone(),
}),
)
.unwrap();
let workers = sloop.workers();
let matched = find_matching_worker(workers, "hello world");
assert!(matched.is_none(), "should not match when no pattern fits");
}
#[test]
fn dispatch_respects_registration_order() {
let counter_a = Arc::new(AtomicUsize::new(0));
let counter_b = Arc::new(AtomicUsize::new(0));
let mut sloop = SensoriumLoop::new();
sloop
.register_worker(
r"run",
Arc::new(CountingWorker {
name: "worker-a",
call_count: counter_a.clone(),
}),
)
.unwrap();
sloop
.register_worker(
r"this",
Arc::new(CountingWorker {
name: "worker-b",
call_count: counter_b.clone(),
}),
)
.unwrap();
let workers = sloop.workers();
let matched = find_matching_worker(workers, "run this");
assert!(matched.is_some());
assert_eq!(
matched.unwrap().worker.name(),
"worker-a",
"first registered worker wins when both patterns match"
);
}
#[test]
fn dispatch_second_worker_matches_when_first_does_not() {
let counter_a = Arc::new(AtomicUsize::new(0));
let counter_b = Arc::new(AtomicUsize::new(0));
let mut sloop = SensoriumLoop::new();
sloop
.register_worker(
r"^/deploy",
Arc::new(CountingWorker {
name: "deploy-worker",
call_count: counter_a.clone(),
}),
)
.unwrap();
sloop
.register_worker(
r"^/status",
Arc::new(CountingWorker {
name: "status-worker",
call_count: counter_b.clone(),
}),
)
.unwrap();
let workers = sloop.workers();
let matched = find_matching_worker(workers, "/status all");
assert!(matched.is_some());
assert_eq!(matched.unwrap().worker.name(), "status-worker");
}
#[test]
fn parse_kv_returns_empty_for_plain_command() {
let args = crate::plugin::parse_kv("/task list");
assert!(args.is_empty());
}
#[test]
fn parse_kv_extracts_key_value_pairs() {
let args =
crate::plugin::parse_kv("/run-task key=PROJ-123 repo_url=git@github.com:org/repo.git");
assert_eq!(args.get("key").map(String::as_str), Some("PROJ-123"));
assert_eq!(
args.get("repo_url").map(String::as_str),
Some("git@github.com:org/repo.git")
);
}
#[test]
fn parse_kv_strips_bot_mention() {
let args = crate::plugin::parse_kv("<@U123ABC> /run-task key=PROJ-1");
assert_eq!(args.get("key").map(String::as_str), Some("PROJ-1"));
}
#[test]
fn parse_kv_handles_quoted_values() {
let args = crate::plugin::parse_kv(
r#"/run-task key="PROJ-123" repo_url="git@github.com:org/repo""#,
);
assert_eq!(args.get("key").map(String::as_str), Some("PROJ-123"));
assert_eq!(
args.get("repo_url").map(String::as_str),
Some("git@github.com:org/repo")
);
}
#[test]
fn parse_kv_keys_are_lowercased() {
let args = crate::plugin::parse_kv("/run-task Key=VALUE");
assert_eq!(args.get("key").map(String::as_str), Some("VALUE"));
}
#[test]
fn parse_kv_empty_string_returns_empty() {
let args = crate::plugin::parse_kv("");
assert!(args.is_empty());
}
#[test]
fn build_help_response_lists_workers_inline() {
let counter = Arc::new(AtomicUsize::new(0));
let mut sloop = SensoriumLoop::new();
sloop
.register_worker(
r"^/alpha",
Arc::new(CountingWorker {
name: "alpha",
call_count: counter.clone(),
}),
)
.unwrap();
sloop
.register_worker(
r"^/beta",
Arc::new(CountingWorker {
name: "beta",
call_count: counter.clone(),
}),
)
.unwrap();
let workers = sloop.workers();
let response = crate::plugin::build_help_response(workers);
assert!(
response.contains("/count \u{2014} counting worker for tests"),
"expected inline format: got {response}"
);
}
}