tail-fin-daemon 0.6.4

Long-running browser-session daemon for tail-fin (tfd binary). Keeps Chrome tabs warm across invocations via a Unix-socket protocol; registers Site implementations through a runtime Arc<dyn Site> registry.
Documentation
//! Command handlers. Each `sa.*`, `twitter.*`, `grok.*` cmd routes to a
//! per-session handler that implements [`SiteHandler`].
//!
//! Adding a new site with command support:
//! 1. Add a handler module here implementing `SiteHandler`
//! 2. Register a factory via `SiteEntry::new` in `main.rs`

pub mod grok;
pub mod sa;
pub mod twitter;

pub(crate) mod params;
pub(crate) mod response;

use async_trait::async_trait;
use night_fury_daemon_core::protocol::Response;
use serde_json::Value;

/// Per-session command executor. One instance lives for the lifetime of a
/// daemon session; implementations may hold state (e.g. a warmed-up client).
#[async_trait]
pub trait SiteHandler: Send + Sync {
    async fn handle(&self, id: &str, cmd: &str, params: &Value) -> Response;
}

/// Fallback handler for sites that have no command support yet. Returns a
/// clear error rather than silently doing nothing.
pub(crate) struct NoopHandler {
    site_id: &'static str,
}

impl NoopHandler {
    pub(crate) fn new(site_id: &'static str) -> Self {
        Self { site_id }
    }
}

#[async_trait]
impl SiteHandler for NoopHandler {
    async fn handle(&self, id: &str, cmd: &str, _params: &Value) -> Response {
        Response::err(
            id,
            format!(
                "site '{}' has no command handlers (got '{cmd}'); \
                 only session lifecycle operations are supported",
                self.site_id
            ),
        )
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use serde_json::json;

    #[tokio::test]
    async fn noop_handler_returns_error_with_site_and_cmd() {
        let h = NoopHandler::new("reddit");
        let resp = h.handle("req-1", "reddit.posts", &json!({})).await;
        let s = serde_json::to_string(&resp).unwrap();
        assert!(s.contains("\"ok\":false"));
        assert!(s.contains("reddit"));
        assert!(s.contains("reddit.posts"));
    }
}