#![warn(clippy::pedantic)]
#![allow(
clippy::missing_errors_doc,
clippy::missing_panics_doc,
clippy::doc_markdown
)]
pub(crate) mod agent;
pub mod audio;
pub mod bench_openrouter;
pub mod board;
pub(crate) mod boot;
pub(crate) mod call_registry;
pub mod channels;
pub(crate) mod chat_history;
pub mod checkpoint;
pub mod config;
pub mod config_db;
pub(crate) mod consensus;
pub mod debug;
pub(crate) mod diff_parse;
pub(crate) mod embedder;
pub(crate) mod extraction;
pub(crate) mod git_commands;
pub mod gui;
pub(crate) mod image_strip;
pub mod jobs;
pub(crate) mod joint_verdict;
pub mod lock_utils;
pub mod logs;
pub mod maintainer;
pub mod management;
pub mod message_router;
pub(crate) mod migrations;
pub(crate) mod onnx;
pub(crate) mod prompt;
pub mod providers;
pub mod registry;
pub(crate) mod research_cancel;
pub mod research_cleanup;
pub(crate) mod retry;
pub(crate) mod role;
pub mod search_engine;
pub mod self_update;
pub mod session;
pub mod shutdown;
pub(crate) mod skills;
pub(crate) mod stats;
pub mod temp_cleanup;
pub mod temp_root;
pub mod ticket_buffer;
pub mod tools;
pub mod turso;
pub mod users;
pub mod util;
pub(crate) mod vector;
pub mod wal_guard;
pub mod workspace;
#[cfg(unix)]
pub use tools::shell::grep_engine::run_engine as run_grep_engine;
#[cfg(all(unix, feature = "grep-engine-e2e"))]
#[doc(hidden)]
pub use tools::shell::grep_engine::try_serve_command_for_test as grep_engine_rewrite_for_test;
use async_trait::async_trait;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::sync::{Arc, OnceLock, RwLock};
use strum::IntoEnumIterator;
use tokio::sync::broadcast;
use tokio_util::sync::CancellationToken;
use crate::session::Session;
use crate::util::UnwrapPoison;
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub(crate) struct DiagnosticsCommands {
pub format: Option<String>,
pub format_check: Option<String>,
pub lint: Option<String>,
pub lint_fix: Option<String>,
pub type_check: Option<String>,
pub build: Option<String>,
pub unit_test: Option<String>,
}
impl DiagnosticsCommands {
pub const COMMAND_COUNT: usize = 7;
pub(crate) const UNIT_TEST_LABEL: &str = "unit-test";
pub const COMMAND_LABELS: [&str; Self::COMMAND_COUNT] = [
"format",
"format-check",
"lint-fix",
"lint",
"type-check",
"build",
Self::UNIT_TEST_LABEL,
];
#[must_use]
pub fn commands(&self) -> [(&'static str, Option<&str>); Self::COMMAND_COUNT] {
[
("format", self.format.as_deref()),
("format-check", self.format_check.as_deref()),
("lint-fix", self.lint_fix.as_deref()),
("lint", self.lint.as_deref()),
("type-check", self.type_check.as_deref()),
("build", self.build.as_deref()),
(Self::UNIT_TEST_LABEL, self.unit_test.as_deref()),
]
}
#[must_use]
pub fn from_buffers(buffers: &[String; Self::COMMAND_COUNT]) -> Self {
Self {
format: crate::util::none_if_empty(&buffers[0]),
format_check: crate::util::none_if_empty(&buffers[1]),
lint_fix: crate::util::none_if_empty(&buffers[2]),
lint: crate::util::none_if_empty(&buffers[3]),
type_check: crate::util::none_if_empty(&buffers[4]),
build: crate::util::none_if_empty(&buffers[5]),
unit_test: crate::util::none_if_empty(&buffers[6]),
}
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.commands().iter().all(|(_, cmd)| cmd.is_none())
}
}
#[derive(
Debug,
Clone,
Copy,
PartialEq,
Eq,
Serialize,
Default,
strum::Display,
strum::AsRefStr,
strum::EnumString,
)]
#[serde(rename_all = "snake_case")]
#[strum(serialize_all = "snake_case")]
pub enum WorkspaceStatus {
#[default]
Pending,
Analyzing,
Ready,
Failed,
}
#[derive(Debug, Clone, Serialize)]
pub struct Workspace {
pub name: String,
pub path: String,
pub status: WorkspaceStatus,
pub maintenance_enabled: bool,
pub paused: bool,
pub maintainer_debounce_mins: i64,
pub maintainer_last_run_at: Option<String>,
pub diagnostics: Option<String>,
pub notes: String,
pub last_analyzed_commit: Option<String>,
pub ephemeral: bool,
}
impl Default for Workspace {
fn default() -> Self {
Self {
name: String::default(),
path: String::default(),
status: WorkspaceStatus::Pending,
maintenance_enabled: bool::default(),
paused: bool::default(),
maintainer_debounce_mins: 5,
maintainer_last_run_at: Option::default(),
diagnostics: Option::default(),
notes: String::default(),
last_analyzed_commit: Option::default(),
ephemeral: bool::default(),
}
}
}
impl Workspace {
pub const MAX_MAINTAINER_DEBOUNCE_MINS: i64 = 240;
#[must_use]
pub fn as_path(&self) -> &Path {
Path::new(&self.path)
}
#[must_use]
pub fn from_path(path: &Path) -> Self {
let stored = crate::util::with_block_in_place(|| {
std::fs::canonicalize(path).unwrap_or_else(|_| path.to_path_buf())
});
Self {
name: last_path_component(&stored),
path: stored.to_string_lossy().to_string(),
..Default::default()
}
}
#[must_use]
pub fn display_name(&self) -> String {
last_path_component(self.as_path())
}
#[must_use]
pub fn ephemeral_run(name: &str, path: &Path) -> Self {
let stored = crate::util::with_block_in_place(|| {
std::fs::canonicalize(path).unwrap_or_else(|_| path.to_path_buf())
});
Self {
name: name.to_string(),
path: stored.to_string_lossy().to_string(),
ephemeral: true,
..Default::default()
}
}
}
fn last_path_component(path: &Path) -> String {
path.file_name()
.and_then(|s| s.to_str())
.unwrap_or("unknown")
.to_string()
}
const NANOID_ALPHABET: &[u8; 64] =
b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_-";
#[must_use]
fn generate_nanoid(length: usize) -> String {
(0..length)
.map(|_| {
let idx = (rand::random::<u8>() % 64) as usize;
NANOID_ALPHABET[idx] as char
})
.collect()
}
#[must_use]
pub fn generate_id() -> String {
generate_nanoid(10)
}
#[must_use]
pub(crate) fn generate_suffix() -> String {
generate_nanoid(6)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BotCommand {
Start,
Clear,
ImageModels,
VideoModels,
Board,
Archive,
Pause,
Unpause,
Maintenance,
MaintenanceOn,
MaintenanceOff,
SwitchRole(Role),
}
#[must_use]
pub fn parse_bot_command(content: &str) -> Option<BotCommand> {
let content = content.trim();
let cmd = content.split_once(' ').map_or(content, |(cmd, _)| cmd);
match cmd.to_ascii_lowercase().as_str() {
"/start" => Some(BotCommand::Start),
"/clear" => Some(BotCommand::Clear),
"/image_models" => Some(BotCommand::ImageModels),
"/video_models" => Some(BotCommand::VideoModels),
"/board" => Some(BotCommand::Board),
"/archive" => Some(BotCommand::Archive),
"/pause" => Some(BotCommand::Pause),
"/unpause" => Some(BotCommand::Unpause),
"/maintenance" => Some(BotCommand::Maintenance),
"/maintenance_on" => Some(BotCommand::MaintenanceOn),
"/maintenance_off" => Some(BotCommand::MaintenanceOff),
other => {
let name = other.strip_prefix('/')?;
Role::iter()
.find(|r| r.as_str() == name)
.map(BotCommand::SwitchRole)
}
}
}
#[derive(Debug, Clone)]
pub struct ChannelMessage {
pub user_name: String,
pub reply_target: String,
pub content: String,
pub channel: String,
pub workspace: String,
pub optimistic_id: Option<String>,
pub callback_query_id: Option<String>,
}
#[derive(Debug, Clone)]
pub struct SendMessage {
pub content: String,
pub recipient: String,
pub reply_markup: Option<serde_json::Value>,
}
#[async_trait]
pub trait Channel: Send + Sync {
async fn send(&self, message: &SendMessage) -> anyhow::Result<()>;
async fn listen(&self, tx: tokio::sync::mpsc::Sender<ChannelMessage>) -> anyhow::Result<()>;
fn name(&self) -> &'static str;
fn as_any(&self) -> &dyn std::any::Any;
async fn start_typing(&self, _recipient: &str) -> anyhow::Result<()> {
Ok(())
}
fn resolve_recipient(&self, _user_name: &str, reply_target: &str) -> Option<String> {
Some(reply_target.to_string())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum ChatDirection {
User,
Agent,
Divider,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum ChatEvent {
Message {
message_id: String,
user_name: String,
content: String,
direction: ChatDirection,
timestamp: String,
channel: String,
agent_role: Option<String>,
workspace: String,
optimistic_id: Option<String>,
},
Typing {
user_name: String,
is_typing: bool,
workspace: String,
},
}
pub static CHAT_BROADCAST: OnceLock<broadcast::Sender<ChatEvent>> = OnceLock::new();
pub static MESSAGE_TX: OnceLock<tokio::sync::mpsc::Sender<ChannelMessage>> = OnceLock::new();
pub static GUI_MESSAGE_TX: OnceLock<tokio::sync::mpsc::UnboundedSender<ChannelMessage>> =
OnceLock::new();
#[derive(Default)]
pub struct ChannelRegistry {
channels: RwLock<HashMap<String, Arc<dyn Channel>>>,
}
impl ChannelRegistry {
pub fn register(&self, channel: Arc<dyn Channel>) {
let name = channel.name().to_string();
let mut map = self.channels.write().unwrap_poison();
if let std::collections::hash_map::Entry::Vacant(entry) = map.entry(name.clone()) {
entry.insert(channel);
} else {
tracing::warn!(channel = %name, "Channel registry: duplicate name — skipping register");
}
}
pub fn get(&self, name: &str) -> Option<Arc<dyn Channel>> {
self.channels.read().unwrap_poison().get(name).cloned()
}
pub fn replace(&self, channel: Arc<dyn Channel>) -> Option<Arc<dyn Channel>> {
let name = channel.name().to_string();
self.channels.write().unwrap_poison().insert(name, channel)
}
pub fn unregister(&self, name: &str) -> Option<Arc<dyn Channel>> {
self.channels.write().unwrap_poison().remove(name)
}
pub fn list(&self) -> Vec<(String, Arc<dyn Channel>)> {
self.channels
.read()
.unwrap_poison()
.iter()
.map(|(k, v)| (k.clone(), Arc::clone(v)))
.collect()
}
}
pub static CHANNEL_REGISTRY: OnceLock<ChannelRegistry> = OnceLock::new();
#[must_use]
pub fn channel_registry() -> &'static ChannelRegistry {
CHANNEL_REGISTRY
.get()
.expect("CHANNEL_REGISTRY not initialized — must be set during bootstrap")
}
#[derive(
Debug,
Clone,
Copy,
PartialEq,
Eq,
Hash,
Serialize,
Deserialize,
strum::Display,
strum::EnumIter,
strum::IntoStaticStr,
)]
#[strum(serialize_all = "lowercase")]
pub enum Role {
Manager,
Engineer,
Analyst,
Coder,
Qa,
Reviewer,
Discovery,
Artist,
Maintainer,
Sanitation,
Assistant,
}
#[derive(Debug, Clone)]
pub(crate) struct ToolCallRecord {
pub tool_name: String,
pub arguments: String,
pub duration_ms: i64,
pub success: bool,
pub error_message: Option<String>,
}
pub struct Agent {
#[expect(clippy::struct_field_names)]
pub(crate) agent_id: String,
role: Role,
pub(crate) session: Session,
workspace: Arc<crate::Workspace>,
tools: Vec<Box<dyn crate::Tool>>,
pub(crate) tool_specs: Vec<ToolSpec>,
cancel_token: CancellationToken,
ticket: Option<crate::board::Ticket>,
generation: u64,
tool_stats: std::sync::Mutex<Vec<crate::ToolCallRecord>>,
pub(crate) user_name: String,
pub(crate) channel: String,
pub(crate) parent_key: Option<crate::registry::ParentKey>,
pub(crate) parent_label: Option<String>,
pub(crate) incoming_rx:
Option<tokio::sync::mpsc::UnboundedReceiver<crate::message_router::AgentJob>>,
pub(crate) round_ts: Option<String>,
pub(crate) first_call_notify: Option<std::sync::Arc<tokio::sync::Notify>>,
pub(crate) failure: Option<String>,
pub(crate) failure_class: Option<crate::retry::FailureClass>,
background_sessions: std::sync::Arc<crate::tools::shell::BackgroundSessions>,
}
#[derive(Clone, Serialize, Deserialize)]
pub(crate) struct Verdict {
#[serde(deserialize_with = "de_verdict_score")]
pub score: u8,
#[serde(rename = "issues")]
pub issues_detected: Vec<String>,
}
#[expect(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
fn de_verdict_score<'de, D>(deserializer: D) -> Result<u8, D::Error>
where
D: serde::Deserializer<'de>,
{
use serde::de::Error;
let value = serde_json::Value::deserialize(deserializer)?;
let Some(n) = value.as_number() else {
return Err(D::Error::custom("verdict score must be a number"));
};
let f = n
.as_f64()
.ok_or_else(|| D::Error::custom("verdict score out of accepted ranges"))?;
if (0.0..=10.0).contains(&f) {
Ok(f.floor() as u8)
} else if (11.0..=100.0).contains(&f) {
Ok((f / 10.0).floor() as u8)
} else if (101.0..=255.0).contains(&f) && n.is_i64() {
Ok(f as u8)
} else {
Err(D::Error::custom("verdict score out of accepted ranges"))
}
}
#[cfg(test)]
mod verdict_score_tests {
use super::*;
#[test]
fn verdict_score_accepts_native_and_percent_bands() {
struct Case {
json: &'static str,
expected: Option<u8>,
}
let cases = [
Case {
json: r#"{"score": 8, "issues": []}"#,
expected: Some(8),
},
Case {
json: r#"{"score": 10, "issues": []}"#,
expected: Some(10),
},
Case {
json: r#"{"score": 8.5, "issues": []}"#,
expected: Some(8),
},
Case {
json: r#"{"score": 10.0, "issues": []}"#,
expected: Some(10),
},
Case {
json: r#"{"score": 85, "issues": []}"#,
expected: Some(8),
},
Case {
json: r#"{"score": 11, "issues": []}"#,
expected: Some(1),
},
Case {
json: r#"{"score": 100, "issues": []}"#,
expected: Some(10),
},
Case {
json: r#"{"score": 85.5, "issues": []}"#,
expected: Some(8),
},
Case {
json: r#"{"score": 101, "issues": []}"#,
expected: Some(101),
},
Case {
json: r#"{"score": 255, "issues": []}"#,
expected: Some(255),
},
Case {
json: r#"{"score": 101.0, "issues": []}"#,
expected: None,
},
Case {
json: r#"{"score": 255.0, "issues": []}"#,
expected: None,
},
Case {
json: r#"{"score": 10.5, "issues": []}"#,
expected: None,
},
Case {
json: r#"{"score": 10.9, "issues": []}"#,
expected: None,
},
Case {
json: r#"{"score": -1, "issues": []}"#,
expected: None,
},
Case {
json: r#"{"score": -1.0, "issues": []}"#,
expected: None,
},
Case {
json: r#"{"score": 100.5, "issues": []}"#,
expected: None,
},
Case {
json: r#"{"score": 300, "issues": []}"#,
expected: None,
},
Case {
json: r#"{"score": "high", "issues": []}"#,
expected: None,
},
];
for case in &cases {
let parsed = serde_json::from_str::<Verdict>(case.json);
match case.expected {
Some(score) => assert_eq!(
parsed.expect("must deserialize").score,
score,
"case: {}",
case.json
),
None => assert!(parsed.is_err(), "must fail closed: {}", case.json),
}
}
}
}
#[derive(Clone, Debug, Deserialize)]
pub(crate) struct SanitationVerdict {
pub pass: bool,
#[serde(default)]
pub garbage_files: Vec<String>,
pub rationale: String,
}
#[derive(Clone, Debug, Deserialize)]
pub(crate) struct EngineerSummary {
#[serde(default)]
pub items: Vec<String>,
pub summary: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct ToolSpec {
pub name: String,
pub description: String,
pub parameters: serde_json::Value,
}
#[async_trait]
pub(crate) trait Tool: Send + Sync {
fn name(&self) -> &'static str;
fn description(&self) -> String {
crate::prompt::load_prompt(&format!("tool/{}.md", self.name()))
}
fn parameters_schema(&self) -> serde_json::Value;
async fn execute(
&self,
ws: &crate::Workspace,
args: serde_json::Value,
) -> anyhow::Result<String>;
fn spec(&self) -> ToolSpec {
ToolSpec {
name: self.name().to_string(),
description: self.description(),
parameters: self.parameters_schema(),
}
}
fn should_scrub_output(&self, _args: &serde_json::Value) -> bool {
true
}
fn side_effects(&self) -> bool {
true
}
fn is_advertised(&self) -> bool {
true
}
fn media_marker(&self) -> Option<&'static str> {
None
}
fn format_media_result(&self, output_path: &Path) -> String {
let marker_prefix = self
.media_marker()
.expect("media tool must define a media marker");
format!("{marker_prefix}{}]", output_path.to_string_lossy())
}
fn preserve_full_output(&self) -> bool {
false
}
fn format_output(&self, output: &str) -> String {
if self.preserve_full_output() {
output.to_string()
} else {
crate::util::truncate_tool_output(output)
}
}
async fn image_payload(
&self,
_ws: &crate::Workspace,
_args: &serde_json::Value,
) -> Option<crate::tools::ImagePayload> {
None
}
}
#[derive(
Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, strum::Display, strum::EnumString,
)]
#[serde(rename_all = "snake_case")]
#[strum(serialize_all = "lowercase")]
pub(crate) enum ChatRole {
System,
User,
Assistant,
Tool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct ChatMessage {
pub role: ChatRole,
pub content: String,
}
impl ChatMessage {
#[must_use]
pub fn system(content: impl Into<String>) -> Self {
Self {
role: ChatRole::System,
content: content.into(),
}
}
#[must_use]
pub fn user(content: impl Into<String>) -> Self {
Self {
role: ChatRole::User,
content: content.into(),
}
}
#[must_use]
pub fn assistant(content: impl Into<String>) -> Self {
Self {
role: ChatRole::Assistant,
content: content.into(),
}
}
#[must_use]
pub fn tool_result(tool_call_id: &str, content: &str) -> Self {
let payload = ToolResultPayload {
tool_call_id: tool_call_id.to_string(),
content: content.to_string(),
};
Self {
role: ChatRole::Tool,
content: serde_json::to_string(&payload)
.expect("ToolResultPayload is always serializable"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct ToolCall {
pub id: String,
pub name: String,
pub arguments: serde_json::Value,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct ToolResultPayload {
pub tool_call_id: String,
pub content: String,
}
#[expect(clippy::struct_field_names)]
#[derive(Debug, Clone, Default, PartialEq)]
pub(crate) struct Reasoning {
pub reasoning: Option<String>,
pub reasoning_content: Option<String>,
pub reasoning_details: Option<serde_json::Value>,
}
impl Reasoning {
#[must_use]
pub fn from_optional_parts(
reasoning: Option<String>,
reasoning_content: Option<String>,
reasoning_details: Option<serde_json::Value>,
) -> Option<Self> {
let details = reasoning_details.filter(|v| !v.is_null());
let this = Self {
reasoning,
reasoning_content,
reasoning_details: details,
};
(!this.is_empty()).then_some(this)
}
#[must_use]
const fn is_empty(&self) -> bool {
self.reasoning.is_none()
&& self.reasoning_content.is_none()
&& self.reasoning_details.is_none()
}
}
#[derive(Debug, Clone, Default)]
pub(crate) struct ProviderUsage {
pub input_tokens: Option<u64>,
pub output_tokens: Option<u64>,
pub cached_input_tokens: Option<u64>,
pub cache_miss_tokens: Option<u64>,
pub cost: Option<f64>,
pub cost_details: Option<serde_json::Value>,
}
#[derive(Debug, Clone, Default)]
pub(crate) struct ChatResponse {
pub text: Option<String>,
pub tool_calls: Vec<ToolCall>,
pub usage: Option<ProviderUsage>,
pub reasoning: Option<Reasoning>,
pub finish_reason: Option<String>,
pub upstream_provider: Option<String>,
pub system_fingerprint: Option<String>,
}
impl ChatResponse {
#[must_use]
pub fn text_or_empty(&self) -> &str {
self.text.as_deref().unwrap_or("")
}
}
pub(crate) const DEFAULT_MAX_TOKENS: u32 = 32_000;
#[derive(Debug, Clone)]
pub(crate) struct ChatRequestMeta {
pub purpose: &'static str,
pub agent_id: String,
pub role: String,
pub workspace: String,
pub ticket_id: Option<String>,
}
#[derive(Debug, Clone)]
pub(crate) struct ChatRequest {
pub messages: Vec<ChatMessage>,
pub tools: Option<Vec<ToolSpec>>,
pub model: String,
pub max_tokens: Option<u32>,
pub reasoning_effort: Option<String>,
pub provider_order: Option<String>,
pub meta: Option<ChatRequestMeta>,
}
pub(crate) type ExtractionValidator<T> = dyn Fn(&T) -> Result<(), String> + Send + Sync;
#[async_trait]
pub(crate) trait Provider: Send + Sync {
#[cfg_attr(
not(test),
expect(
dead_code,
reason = "Default kept for test doubles; production dispatches chat_scoped"
)
)]
async fn chat(&self, request: ChatRequest) -> anyhow::Result<ChatResponse> {
let deadline = std::time::Instant::now() + crate::retry::DEFAULT_OPERATION_TIMEOUT;
self.chat_scoped(request, crate::retry::DEFAULT_IDLE_TIMEOUT, deadline)
.await
.map_err(|e| e.inner)
}
async fn chat_scoped(
&self,
request: ChatRequest,
idle_timeout: std::time::Duration,
deadline: std::time::Instant,
) -> Result<ChatResponse, crate::providers::ScopedCallError>;
async fn warmup(&self) -> anyhow::Result<()> {
Ok(())
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct Skill {
pub name: String,
pub description: String,
pub location: PathBuf,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn diagnostics_commands_returns_all_7_in_order() {
let diag = DiagnosticsCommands {
format: Some("cargo fmt".into()),
format_check: Some("cargo fmt -- --check".into()),
lint: Some("cargo clippy -- -D warnings".into()),
lint_fix: Some("cargo clippy --fix --allow-dirty".into()),
type_check: Some("cargo check".into()),
build: Some("cargo build".into()),
unit_test: Some("cargo test".into()),
};
let cmds = diag.commands();
assert_eq!(cmds.len(), 7);
assert_eq!(cmds[0].0, "format");
assert_eq!(cmds[1].0, "format-check");
assert_eq!(cmds[2].0, "lint-fix");
assert_eq!(cmds[3].0, "lint");
assert_eq!(cmds[4].0, "type-check");
assert_eq!(cmds[5].0, "build");
assert_eq!(cmds[6].0, "unit-test");
assert_eq!(cmds[0].1, Some("cargo fmt"));
assert_eq!(cmds[6].1, Some("cargo test"));
}
#[test]
fn diagnostics_is_empty() {
let empty = DiagnosticsCommands::default();
assert!(empty.is_empty());
let partial = DiagnosticsCommands {
format: Some("cargo fmt".into()),
..Default::default()
};
assert!(!partial.is_empty());
}
#[test]
fn from_buffers_empty_strings_become_none() {
let buffers = [const { String::new() }; DiagnosticsCommands::COMMAND_COUNT];
let cmds = DiagnosticsCommands::from_buffers(&buffers);
assert!(cmds.is_empty());
assert!(cmds.format.is_none());
assert!(cmds.unit_test.is_none());
}
#[test]
fn commands_and_from_buffers_are_consistent() {
let original = DiagnosticsCommands {
format: Some("fmt".into()),
format_check: Some("fmt-check".into()),
lint_fix: Some("lint-fix".into()),
lint: Some("lint".into()),
type_check: Some("type-check".into()),
build: Some("build".into()),
unit_test: Some("test".into()),
};
let cmds = original.commands();
let buffers: [String; DiagnosticsCommands::COMMAND_COUNT] =
std::array::from_fn(|i| cmds[i].1.unwrap_or("").to_string());
let restored = DiagnosticsCommands::from_buffers(&buffers);
assert_eq!(restored.format, original.format);
assert_eq!(restored.format_check, original.format_check);
assert_eq!(restored.lint_fix, original.lint_fix);
assert_eq!(restored.lint, original.lint);
assert_eq!(restored.type_check, original.type_check);
assert_eq!(restored.build, original.build);
assert_eq!(restored.unit_test, original.unit_test);
}
#[test]
fn parse_bot_command_coverage() {
use BotCommand::*;
let cases: Vec<(&str, Option<BotCommand>)> = vec![
("/start", Some(Start)),
("/STart", Some(Start)),
("/Start", Some(Start)),
("/START", Some(Start)),
("/start foo", Some(Start)),
("/start ", Some(Start)),
(" /start", Some(Start)),
(" /start ", Some(Start)),
(" /start foo ", Some(Start)),
("/clear", Some(Clear)),
("/CLEAR", Some(Clear)),
("/clear session", Some(Clear)),
(" /clear ", Some(Clear)),
("/image_models", Some(ImageModels)),
("/IMAGE_MODELS", Some(ImageModels)),
("/image_models foo", Some(ImageModels)),
(" /image_models ", Some(ImageModels)),
("/video_models", Some(VideoModels)),
("/Video_Models", Some(VideoModels)),
("/video_models foo", Some(VideoModels)),
("/board", Some(Board)),
("/BOARD", Some(Board)),
("/board foo", Some(Board)),
("/archive", Some(Archive)),
("/archive foo", Some(Archive)),
("/pause", Some(Pause)),
("/pause foo", Some(Pause)),
("/unpause", Some(Unpause)),
("/unpause foo", Some(Unpause)),
("/maintenance", Some(Maintenance)),
("/maintenance on", Some(Maintenance)),
("/maintenance off", Some(Maintenance)),
("/maintenance_on", Some(MaintenanceOn)),
("/MAINTENANCE_ON", Some(MaintenanceOn)),
("/maintenance_on ", Some(MaintenanceOn)),
("/maintenance_off", Some(MaintenanceOff)),
("/Maintenance_Off", Some(MaintenanceOff)),
("/maintenance_off foo", Some(MaintenanceOff)),
("/manager", Some(SwitchRole(Role::Manager))),
("/engineer", Some(SwitchRole(Role::Engineer))),
("/analyst", Some(SwitchRole(Role::Analyst))),
("/coder", Some(SwitchRole(Role::Coder))),
("/qa", Some(SwitchRole(Role::Qa))),
("/reviewer", Some(SwitchRole(Role::Reviewer))),
("/discovery", Some(SwitchRole(Role::Discovery))),
("/artist", Some(SwitchRole(Role::Artist))),
("/maintainer", Some(SwitchRole(Role::Maintainer))),
("/sanitation", Some(SwitchRole(Role::Sanitation))),
("/assistant", Some(SwitchRole(Role::Assistant))),
("/ARTIST", Some(SwitchRole(Role::Artist))),
("/engineer foo", Some(SwitchRole(Role::Engineer))),
(" /coder ", Some(SwitchRole(Role::Coder))),
("/", None),
("/s", None),
("/stard", None),
("/started", None),
("/cleared", None),
("/model", None),
("/models", None), ("/image", None),
("/video", None),
("/boardx", None),
("/maintenance_onn", None),
("/maintenance_o", None),
("/engineerr", None),
("/managr", None),
("/artiste", None),
("/ reset", None),
("start", None),
("clear", None),
("models", None),
("", None),
(" ", None),
("not a command", None),
];
for (input, expected) in cases {
assert_eq!(parse_bot_command(input), expected, "input: {input:?}");
}
}
}