zeroclaw/src/integrations/mod.rs
Chummy 49fcc7a2c4
test: deepen and complete project-wide test coverage (#297)
* test: deepen coverage for health doctor provider and tunnels

* test: add broad trait and module re-export coverage
2026-02-16 05:58:24 -05:00

227 lines
7.3 KiB
Rust

pub mod registry;
use crate::config::Config;
use anyhow::Result;
/// Integration status
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum IntegrationStatus {
/// Fully implemented and ready to use
Available,
/// Configured and active
Active,
/// Planned but not yet implemented
ComingSoon,
}
/// Integration category
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum IntegrationCategory {
Chat,
AiModel,
Productivity,
MusicAudio,
SmartHome,
ToolsAutomation,
MediaCreative,
Social,
Platform,
}
impl IntegrationCategory {
pub fn label(self) -> &'static str {
match self {
Self::Chat => "Chat Providers",
Self::AiModel => "AI Models",
Self::Productivity => "Productivity",
Self::MusicAudio => "Music & Audio",
Self::SmartHome => "Smart Home",
Self::ToolsAutomation => "Tools & Automation",
Self::MediaCreative => "Media & Creative",
Self::Social => "Social",
Self::Platform => "Platforms",
}
}
pub fn all() -> &'static [Self] {
&[
Self::Chat,
Self::AiModel,
Self::Productivity,
Self::MusicAudio,
Self::SmartHome,
Self::ToolsAutomation,
Self::MediaCreative,
Self::Social,
Self::Platform,
]
}
}
/// A registered integration
pub struct IntegrationEntry {
pub name: &'static str,
pub description: &'static str,
pub category: IntegrationCategory,
pub status_fn: fn(&Config) -> IntegrationStatus,
}
/// Handle the `integrations` CLI command
pub fn handle_command(command: crate::IntegrationCommands, config: &Config) -> Result<()> {
match command {
crate::IntegrationCommands::Info { name } => show_integration_info(config, &name),
}
}
fn show_integration_info(config: &Config, name: &str) -> Result<()> {
let entries = registry::all_integrations();
let name_lower = name.to_lowercase();
let Some(entry) = entries.iter().find(|e| e.name.to_lowercase() == name_lower) else {
anyhow::bail!(
"Unknown integration: {name}. Check README for supported integrations or run `zeroclaw onboard --interactive` to configure channels/providers."
);
};
let status = (entry.status_fn)(config);
let (icon, label) = match status {
IntegrationStatus::Active => ("", "Active"),
IntegrationStatus::Available => ("", "Available"),
IntegrationStatus::ComingSoon => ("🔜", "Coming Soon"),
};
println!();
println!(
" {} {}{}",
icon,
console::style(entry.name).white().bold(),
entry.description
);
println!(" Category: {}", entry.category.label());
println!(" Status: {label}");
println!();
// Show setup hints based on integration
match entry.name {
"Telegram" => {
println!(" Setup:");
println!(" 1. Message @BotFather on Telegram");
println!(" 2. Create a bot and copy the token");
println!(" 3. Run: zeroclaw onboard");
println!(" 4. Start: zeroclaw channel start");
}
"Discord" => {
println!(" Setup:");
println!(" 1. Go to https://discord.com/developers/applications");
println!(" 2. Create app → Bot → Copy token");
println!(" 3. Enable MESSAGE CONTENT intent");
println!(" 4. Run: zeroclaw onboard");
}
"Slack" => {
println!(" Setup:");
println!(" 1. Go to https://api.slack.com/apps");
println!(" 2. Create app → Bot Token Scopes → Install");
println!(" 3. Run: zeroclaw onboard");
}
"OpenRouter" => {
println!(" Setup:");
println!(" 1. Get API key at https://openrouter.ai/keys");
println!(" 2. Run: zeroclaw onboard");
println!(" Access 200+ models with one key.");
}
"Ollama" => {
println!(" Setup:");
println!(" 1. Install: brew install ollama");
println!(" 2. Pull a model: ollama pull llama3");
println!(" 3. Set provider to 'ollama' in config.toml");
}
"iMessage" => {
println!(" Setup (macOS only):");
println!(" Uses AppleScript bridge to send/receive iMessages.");
println!(" Requires Full Disk Access in System Settings → Privacy.");
}
"GitHub" => {
println!(" Setup:");
println!(" 1. Create a personal access token at https://github.com/settings/tokens");
println!(" 2. Add to config: [integrations.github] token = \"ghp_...\"");
}
"Browser" => {
println!(" Built-in:");
println!(" ZeroClaw can control Chrome/Chromium for web tasks.");
println!(" Uses headless browser automation.");
}
"Cron" => {
println!(" Built-in:");
println!(" Schedule tasks in ~/.zeroclaw/workspace/cron/");
println!(" Run: zeroclaw cron list");
}
"Webhooks" => {
println!(" Built-in:");
println!(" HTTP endpoint for external triggers.");
println!(" Run: zeroclaw gateway");
}
_ => {
if status == IntegrationStatus::ComingSoon {
println!(" This integration is planned. Stay tuned!");
println!(" Track progress: https://github.com/theonlyhennygod/zeroclaw");
}
}
}
println!();
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn integration_category_all_includes_every_variant_once() {
let all = IntegrationCategory::all();
assert_eq!(all.len(), 9);
let labels: Vec<&str> = all.iter().map(|cat| cat.label()).collect();
assert!(labels.contains(&"Chat Providers"));
assert!(labels.contains(&"AI Models"));
assert!(labels.contains(&"Productivity"));
assert!(labels.contains(&"Music & Audio"));
assert!(labels.contains(&"Smart Home"));
assert!(labels.contains(&"Tools & Automation"));
assert!(labels.contains(&"Media & Creative"));
assert!(labels.contains(&"Social"));
assert!(labels.contains(&"Platforms"));
}
#[test]
fn handle_command_info_is_case_insensitive_for_known_integrations() {
let config = Config::default();
let first_name = registry::all_integrations()
.first()
.expect("registry should define at least one integration")
.name
.to_lowercase();
let result = handle_command(
crate::IntegrationCommands::Info { name: first_name },
&config,
);
assert!(result.is_ok());
}
#[test]
fn handle_command_info_returns_error_for_unknown_integration() {
let config = Config::default();
let result = handle_command(
crate::IntegrationCommands::Info {
name: "definitely-not-a-real-integration".into(),
},
&config,
);
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(err.contains("Unknown integration"));
}
}