use std::sync::Arc;
use async_trait::async_trait;
use serde_json::{json, Value};
use car_engine::messaging::{MessageReceipt, OutboundMessage, Recipient};
use car_engine::ToolExecutor;
use car_messaging::outbound::OutboundAdapter;
pub const HOST_CHANNEL_SEND_TOOL: &str = "messaging.channel_send";
pub const HOST_CHANNEL: &str = "host";
pub struct HostChannelAdapter {
executor: Arc<dyn ToolExecutor>,
}
impl HostChannelAdapter {
pub fn new(executor: Arc<dyn ToolExecutor>) -> Self {
Self { executor }
}
}
#[async_trait]
impl OutboundAdapter for HostChannelAdapter {
fn channel(&self) -> &str {
HOST_CHANNEL
}
async fn send(&self, msg: &OutboundMessage) -> Result<MessageReceipt, String> {
let (kind, to) = match &msg.to {
Recipient::Direct(handle) => ("direct", handle.as_str()),
Recipient::Channel(id) => ("channel", id.as_str()),
};
let params = json!({
"channel": msg.channel,
"kind": kind,
"to": to,
"body": msg.body,
});
match self.executor.execute(HOST_CHANNEL_SEND_TOOL, ¶ms).await {
Ok(value) => Ok(receipt_from_host(&msg.channel, &value)),
Err(e) if e.starts_with("unknown tool") => Err(format!(
"no transport for messaging channel '{}': this host does not implement the \
'{}' tool callback. A host that can deliver on '{}' should handle \
'{}' with parameters {{channel, kind, to, body}} and return \
{{\"message_id\": \"…\"}} (message_id optional).",
msg.channel, HOST_CHANNEL_SEND_TOOL, msg.channel, HOST_CHANNEL_SEND_TOOL
)),
Err(e) => Err(e),
}
}
}
fn receipt_from_host(channel: &str, value: &Value) -> MessageReceipt {
let receipt = MessageReceipt::delivered(channel);
match value
.get("message_id")
.and_then(|v| v.as_str())
.filter(|s| !s.trim().is_empty())
{
Some(id) => receipt.with_message_id(id),
None => receipt,
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::Mutex;
struct FakeExecutor {
seen: Mutex<Vec<(String, Value)>>,
result: Result<Value, String>,
}
impl FakeExecutor {
fn new(result: Result<Value, String>) -> Arc<Self> {
Arc::new(Self {
seen: Mutex::new(Vec::new()),
result,
})
}
fn last(&self) -> (String, Value) {
self.seen.lock().unwrap().last().cloned().expect("no call")
}
fn calls(&self) -> usize {
self.seen.lock().unwrap().len()
}
}
#[async_trait]
impl ToolExecutor for FakeExecutor {
async fn execute(&self, tool: &str, params: &Value) -> Result<Value, String> {
self.seen
.lock()
.unwrap()
.push((tool.to_string(), params.clone()));
self.result.clone()
}
}
fn msg(kind_channel: bool) -> OutboundMessage {
OutboundMessage {
channel: "teams".to_string(),
to: if kind_channel {
Recipient::Channel("19:meeting@thread.v2".to_string())
} else {
Recipient::Direct("keenan@parslee.ai".to_string())
},
body: "deploy is green".to_string(),
idempotency_key: Some("run-42".to_string()),
}
}
#[tokio::test]
async fn direct_send_builds_the_expected_callback() {
let exec = FakeExecutor::new(Ok(json!({ "message_id": "1700000000.1" })));
let adapter = HostChannelAdapter::new(exec.clone());
let receipt = adapter.send(&msg(false)).await.unwrap();
assert_eq!(receipt.channel, "teams");
assert_eq!(receipt.message_id.as_deref(), Some("1700000000.1"));
assert!(!receipt.deduplicated);
let (tool, params) = exec.last();
assert_eq!(tool, HOST_CHANNEL_SEND_TOOL);
assert_eq!(
params,
json!({
"channel": "teams",
"kind": "direct",
"to": "keenan@parslee.ai",
"body": "deploy is green",
})
);
}
#[tokio::test]
async fn channel_send_uses_the_channel_kind() {
let exec = FakeExecutor::new(Ok(json!({})));
let adapter = HostChannelAdapter::new(exec.clone());
let receipt = adapter.send(&msg(true)).await.unwrap();
assert_eq!(receipt.message_id, None);
let (_, params) = exec.last();
assert_eq!(params["kind"], "channel");
assert_eq!(params["to"], "19:meeting@thread.v2");
}
#[tokio::test]
async fn unknown_tool_becomes_an_actionable_message() {
let exec = FakeExecutor::new(Err("unknown tool: 'messaging.channel_send'".to_string()));
let adapter = HostChannelAdapter::new(exec.clone());
let err = adapter.send(&msg(false)).await.unwrap_err();
assert!(
!err.contains("unknown tool"),
"the raw sentinel must not surface: {err}"
);
assert!(
err.contains("no transport for messaging channel 'teams'"),
"{err}"
);
assert!(err.contains(HOST_CHANNEL_SEND_TOOL), "{err}");
assert_eq!(exec.calls(), 1);
}
#[tokio::test]
async fn a_host_error_passes_through() {
let exec = FakeExecutor::new(Err("not a member of that team".to_string()));
let adapter = HostChannelAdapter::new(exec.clone());
let err = adapter.send(&msg(false)).await.unwrap_err();
assert_eq!(err, "not a member of that team");
}
#[tokio::test]
async fn a_blank_message_id_is_treated_as_absent() {
let exec = FakeExecutor::new(Ok(json!({ "message_id": " " })));
let adapter = HostChannelAdapter::new(exec.clone());
let receipt = adapter.send(&msg(false)).await.unwrap();
assert_eq!(receipt.message_id, None);
}
}