use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::collections::HashMap;
use std::path::Path;
pub const DEFAULT_MODEL: &str = "gemini-3.5-flash";
pub const DEFAULT_IMAGE_GENERATION_MODEL: &str = "gemini-3.1-flash-image-preview";
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum ThinkingLevel {
Minimal,
Low,
Medium,
High,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct GenerationConfig {
#[serde(skip_serializing_if = "Option::is_none")]
pub thinking_level: Option<ThinkingLevel>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ModelEntry {
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub api_key: Option<String>,
#[serde(default)]
pub generation: GenerationConfig,
}
impl Default for ModelEntry {
fn default() -> Self {
Self {
name: DEFAULT_MODEL.to_string(),
api_key: None,
generation: GenerationConfig::default(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ModelConfig {
#[serde(default = "default_model_entry")]
pub default: ModelEntry,
#[serde(default = "default_image_generation_entry")]
pub image_generation: ModelEntry,
}
fn default_model_entry() -> ModelEntry {
ModelEntry {
name: DEFAULT_MODEL.to_string(),
api_key: None,
generation: GenerationConfig::default(),
}
}
fn default_image_generation_entry() -> ModelEntry {
ModelEntry {
name: DEFAULT_IMAGE_GENERATION_MODEL.to_string(),
api_key: None,
generation: GenerationConfig::default(),
}
}
impl Default for ModelConfig {
fn default() -> Self {
Self {
default: default_model_entry(),
image_generation: default_image_generation_entry(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct GeminiConfig {
#[serde(skip_serializing_if = "Option::is_none")]
pub api_key: Option<String>,
#[serde(default)]
pub vertex: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub project: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub location: Option<String>,
#[serde(default)]
pub models: ModelConfig,
#[serde(skip_serializing_if = "Option::is_none")]
pub enable_google_search: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub enable_url_context: Option<bool>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SystemInstructionSection {
pub content: String,
pub title: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CustomSystemInstructions {
pub text: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AppendedSystemInstructions {
#[serde(skip_serializing_if = "Option::is_none")]
pub custom_identity: Option<String>,
#[serde(default)]
pub appended_sections: Vec<SystemInstructionSection>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum SystemInstructions {
Custom(CustomSystemInstructions),
Appended(AppendedSystemInstructions),
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
pub enum BuiltinTools {
#[serde(rename = "CREATE_FILE")]
CreateFile,
#[serde(rename = "EDIT_FILE")]
EditFile,
#[serde(rename = "FIND_FILE")]
FindFile,
#[serde(rename = "LIST_DIR")]
ListDir,
#[serde(rename = "RUN_COMMAND")]
RunCommand,
#[serde(rename = "SEARCH_DIR")]
SearchDir,
#[serde(rename = "VIEW_FILE")]
ViewFile,
#[serde(rename = "START_SUBAGENT")]
StartSubagent,
#[serde(rename = "GENERATE_IMAGE")]
GenerateImage,
#[serde(rename = "FINISH")]
Finish,
}
impl BuiltinTools {
pub const fn as_str(&self) -> &'static str {
match self {
Self::CreateFile => "CREATE_FILE",
Self::EditFile => "EDIT_FILE",
Self::FindFile => "FIND_FILE",
Self::ListDir => "LIST_DIR",
Self::RunCommand => "RUN_COMMAND",
Self::SearchDir => "SEARCH_DIR",
Self::ViewFile => "VIEW_FILE",
Self::StartSubagent => "START_SUBAGENT",
Self::GenerateImage => "GENERATE_IMAGE",
Self::Finish => "FINISH",
}
}
pub fn read_only() -> Vec<Self> {
vec![
Self::FindFile,
Self::ListDir,
Self::ViewFile,
Self::SearchDir,
]
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct CapabilitiesConfig {
#[serde(skip_serializing_if = "Option::is_none")]
pub enabled_tools: Option<Vec<BuiltinTools>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub disabled_tools: Option<Vec<BuiltinTools>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub compaction_threshold: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub finish_tool_schema_json: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub image_model: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum McpServerConfig {
#[serde(rename = "stdio")]
Stdio {
name: String,
command: String,
args: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
enabled_tools: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
disabled_tools: Option<Vec<String>>,
},
#[serde(rename = "sse")]
Sse {
name: String,
url: String,
#[serde(skip_serializing_if = "Option::is_none")]
headers: Option<HashMap<String, String>>,
#[serde(skip_serializing_if = "Option::is_none")]
enabled_tools: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
disabled_tools: Option<Vec<String>>,
},
#[serde(rename = "http")]
Http {
name: String,
url: String,
#[serde(skip_serializing_if = "Option::is_none")]
headers: Option<HashMap<String, String>>,
#[serde(default = "default_mcp_timeout")]
timeout: f64,
#[serde(default = "default_mcp_sse_timeout")]
sse_read_timeout: f64,
#[serde(default = "default_true")]
terminate_on_close: bool,
#[serde(skip_serializing_if = "Option::is_none")]
enabled_tools: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
disabled_tools: Option<Vec<String>>,
},
}
impl McpServerConfig {
pub fn name(&self) -> &str {
match self {
Self::Stdio { name, .. } | Self::Sse { name, .. } | Self::Http { name, .. } => name,
}
}
}
#[derive(Debug, Clone)]
pub struct AntigravityExecutionError {
pub message: String,
}
impl std::fmt::Display for AntigravityExecutionError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Terminal execution error: {}", self.message)
}
}
impl std::error::Error for AntigravityExecutionError {}
const fn default_mcp_timeout() -> f64 {
30.0
}
const fn default_mcp_sse_timeout() -> f64 {
300.0
}
const fn default_true() -> bool {
true
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolCall {
pub id: String,
pub name: String,
pub args: Value,
#[serde(skip_serializing_if = "Option::is_none")]
pub canonical_path: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolResult {
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub result: Option<Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub error: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct UsageMetadata {
pub prompt_token_count: i32,
pub candidates_token_count: i32,
pub total_token_count: i32,
#[serde(default)]
pub cached_content_token_count: i32,
#[serde(default)]
pub thoughts_token_count: i32,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
pub enum StepType {
#[serde(rename = "TEXT_RESPONSE")]
TextResponse,
#[serde(rename = "TOOL_CALL")]
ToolCall,
#[serde(rename = "SYSTEM_MESSAGE")]
SystemMessage,
#[serde(rename = "COMPACTION")]
Compaction,
#[serde(rename = "FINISH")]
Finish,
#[serde(rename = "UNKNOWN")]
Unknown,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
pub enum StepSource {
#[serde(rename = "SYSTEM")]
System,
#[serde(rename = "USER")]
User,
#[serde(rename = "MODEL")]
Model,
#[serde(rename = "UNKNOWN")]
Unknown,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
pub enum StepTarget {
#[serde(rename = "TARGET_USER")]
User,
#[serde(rename = "TARGET_ENVIRONMENT")]
Environment,
#[serde(rename = "TARGET_UNSPECIFIED")]
Unspecified,
#[serde(rename = "UNKNOWN")]
Unknown,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
pub enum StepStatus {
#[serde(rename = "ACTIVE")]
Active,
#[serde(rename = "DONE")]
Done,
#[serde(rename = "WAITING_FOR_USER")]
WaitingForUser,
#[serde(rename = "ERROR")]
Error,
#[serde(rename = "CANCELED")]
Canceled,
#[serde(rename = "TERMINAL_ERROR")]
TerminalError,
#[serde(rename = "UNKNOWN")]
Unknown,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Step {
pub id: String,
pub step_index: u32,
pub r#type: StepType,
pub source: StepSource,
pub target: StepTarget,
pub status: StepStatus,
pub content: String,
pub content_delta: String,
pub thinking: String,
pub thinking_delta: String,
pub tool_calls: Vec<ToolCall>,
pub error: String,
pub is_complete_response: Option<bool>,
pub structured_output: Option<Value>,
pub usage_metadata: Option<UsageMetadata>,
#[serde(default)]
pub cascade_id: String,
#[serde(default)]
pub trajectory_id: String,
#[serde(default)]
pub http_code: u32,
}
impl Default for Step {
fn default() -> Self {
Self {
id: String::new(),
step_index: 0,
r#type: StepType::Unknown,
source: StepSource::Unknown,
target: StepTarget::Unknown,
status: StepStatus::Unknown,
content: String::new(),
content_delta: String::new(),
thinking: String::new(),
thinking_delta: String::new(),
tool_calls: Vec::new(),
error: String::new(),
is_complete_response: None,
structured_output: None,
usage_metadata: None,
cascade_id: String::new(),
trajectory_id: String::new(),
http_code: 0,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct HookResult {
pub allow: bool,
#[serde(default)]
pub message: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct QuestionResponse {
pub selected_option_ids: Option<Vec<String>>,
#[serde(default)]
pub freeform_response: String,
#[serde(default)]
pub skipped: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct QuestionHookResult {
pub responses: Vec<QuestionResponse>,
#[serde(default)]
pub cancelled: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AskQuestionOption {
pub id: String,
pub text: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AskQuestionEntry {
pub question: String,
pub options: Vec<AskQuestionOption>,
#[serde(default)]
pub is_multi_select: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ChatResponse {
pub text: String,
pub thinking: String,
pub steps: Vec<Step>,
pub usage_metadata: UsageMetadata,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "chunk_type")]
pub enum StreamChunk {
Thought {
step_index: u32,
text: String,
},
Text {
step_index: u32,
text: String,
},
ToolCall(ToolCall),
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
pub enum ImageMime {
#[serde(rename = "image/bmp")]
Bmp,
#[serde(rename = "image/jpeg")]
Jpeg,
#[serde(rename = "image/png")]
Png,
#[serde(rename = "image/webp")]
Webp,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
pub enum DocumentMime {
#[serde(rename = "application/pdf")]
Pdf,
#[serde(rename = "application/json")]
Json,
#[serde(rename = "text/css")]
Css,
#[serde(rename = "text/csv")]
Csv,
#[serde(rename = "text/html")]
Html,
#[serde(rename = "application/javascript")]
Javascript,
#[serde(rename = "text/plain")]
PlainText,
#[serde(rename = "text/rtf")]
Rtf,
#[serde(rename = "application/xml")]
Xml,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
pub enum AudioMime {
#[serde(rename = "audio/wav")]
Wav,
#[serde(rename = "audio/mp3")]
Mp3,
#[serde(rename = "audio/aac")]
Aac,
#[serde(rename = "audio/ogg")]
Ogg,
#[serde(rename = "audio/flac")]
Flac,
#[serde(rename = "audio/opus")]
Opus,
#[serde(rename = "audio/mpeg")]
Mpeg,
#[serde(rename = "audio/m4a")]
M4a,
#[serde(rename = "audio/l16")]
L16,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
pub enum VideoMime {
#[serde(rename = "video/3gpp")]
Threegpp,
#[serde(rename = "video/x-msvideo")]
Avi,
#[serde(rename = "video/mp4")]
Mp4,
#[serde(rename = "video/mpeg")]
VideoMpeg,
#[serde(rename = "video/mpg")]
Mpg,
#[serde(rename = "video/quicktime")]
Quicktime,
#[serde(rename = "video/webm")]
Webm,
#[serde(rename = "video/x-ms-wmv")]
Wmv,
#[serde(rename = "video/x-flv")]
XFlv,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(untagged)]
pub enum MimeType {
Image(ImageMime),
Document(DocumentMime),
Audio(AudioMime),
Video(VideoMime),
}
impl MimeType {
pub const fn as_str(&self) -> &'static str {
match self {
Self::Image(m) => match m {
ImageMime::Bmp => "image/bmp",
ImageMime::Jpeg => "image/jpeg",
ImageMime::Png => "image/png",
ImageMime::Webp => "image/webp",
},
Self::Document(m) => match m {
DocumentMime::Pdf => "application/pdf",
DocumentMime::Json => "application/json",
DocumentMime::Css => "text/css",
DocumentMime::Csv => "text/csv",
DocumentMime::Html => "text/html",
DocumentMime::Javascript => "application/javascript",
DocumentMime::PlainText => "text/plain",
DocumentMime::Rtf => "text/rtf",
DocumentMime::Xml => "application/xml",
},
Self::Audio(m) => match m {
AudioMime::Wav => "audio/wav",
AudioMime::Mp3 => "audio/mp3",
AudioMime::Aac => "audio/aac",
AudioMime::Ogg => "audio/ogg",
AudioMime::Flac => "audio/flac",
AudioMime::Opus => "audio/opus",
AudioMime::Mpeg => "audio/mpeg",
AudioMime::M4a => "audio/m4a",
AudioMime::L16 => "audio/l16",
},
Self::Video(m) => match m {
VideoMime::Threegpp => "video/3gpp",
VideoMime::Avi => "video/x-msvideo",
VideoMime::Mp4 => "video/mp4",
VideoMime::VideoMpeg => "video/mpeg",
VideoMime::Mpg => "video/mpg",
VideoMime::Quicktime => "video/quicktime",
VideoMime::Webm => "video/webm",
VideoMime::Wmv => "video/x-ms-wmv",
VideoMime::XFlv => "video/x-flv",
},
}
}
}
impl std::fmt::Display for MimeType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Media {
pub data: Vec<u8>,
pub mime_type: MimeType,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
}
pub type Image = Media;
pub type Document = Media;
pub type Audio = Media;
pub type Video = Media;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum ContentPrimitive {
Text(String),
Media(Media),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum Content {
Single(ContentPrimitive),
Multi(Vec<ContentPrimitive>),
}
impl Content {
pub fn text(s: impl Into<String>) -> Self {
Self::Single(ContentPrimitive::Text(s.into()))
}
pub const fn media(media: Media) -> Self {
Self::Single(ContentPrimitive::Media(media))
}
pub fn from_file(path: impl AsRef<Path>, description: Option<&str>) -> Result<Self, String> {
let path = path.as_ref();
let data = std::fs::read(path).map_err(|e| format!("Failed to read file: {e}"))?;
let ext = path
.extension()
.and_then(|e| e.to_str())
.unwrap_or("")
.to_lowercase();
let mime_type = mime_from_extension(&ext)
.ok_or_else(|| format!("Unsupported file extension: .{ext}"))?;
Ok(Self::Single(ContentPrimitive::Media(Media {
data,
mime_type,
description: description.map(String::from),
})))
}
pub fn as_text(&self) -> Option<&str> {
match self {
Self::Single(ContentPrimitive::Text(s)) => Some(s),
_ => None,
}
}
}
impl From<&str> for Content {
fn from(s: &str) -> Self {
Self::text(s)
}
}
impl From<String> for Content {
fn from(s: String) -> Self {
Self::text(s)
}
}
fn mime_from_extension(ext: &str) -> Option<MimeType> {
match ext {
"bmp" => Some(MimeType::Image(ImageMime::Bmp)),
"jpg" | "jpeg" => Some(MimeType::Image(ImageMime::Jpeg)),
"png" => Some(MimeType::Image(ImageMime::Png)),
"webp" => Some(MimeType::Image(ImageMime::Webp)),
"pdf" => Some(MimeType::Document(DocumentMime::Pdf)),
"json" => Some(MimeType::Document(DocumentMime::Json)),
"css" => Some(MimeType::Document(DocumentMime::Css)),
"csv" => Some(MimeType::Document(DocumentMime::Csv)),
"html" | "htm" => Some(MimeType::Document(DocumentMime::Html)),
"js" | "mjs" => Some(MimeType::Document(DocumentMime::Javascript)),
"txt" | "text" | "md" | "log" => Some(MimeType::Document(DocumentMime::PlainText)),
"rtf" => Some(MimeType::Document(DocumentMime::Rtf)),
"xml" => Some(MimeType::Document(DocumentMime::Xml)),
"wav" => Some(MimeType::Audio(AudioMime::Wav)),
"mp3" => Some(MimeType::Audio(AudioMime::Mp3)),
"aac" => Some(MimeType::Audio(AudioMime::Aac)),
"ogg" | "oga" => Some(MimeType::Audio(AudioMime::Ogg)),
"flac" => Some(MimeType::Audio(AudioMime::Flac)),
"opus" => Some(MimeType::Audio(AudioMime::Opus)),
"m4a" => Some(MimeType::Audio(AudioMime::M4a)),
"3gp" | "3gpp" => Some(MimeType::Video(VideoMime::Threegpp)),
"avi" => Some(MimeType::Video(VideoMime::Avi)),
"mp4" | "m4v" => Some(MimeType::Video(VideoMime::Mp4)),
"mpeg" | "mpg" => Some(MimeType::Video(VideoMime::VideoMpeg)),
"mov" => Some(MimeType::Video(VideoMime::Quicktime)),
"webm" => Some(MimeType::Video(VideoMime::Webm)),
"wmv" => Some(MimeType::Video(VideoMime::Wmv)),
"flv" => Some(MimeType::Video(VideoMime::XFlv)),
_ => None,
}
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum TriggerDelivery {
SendImmediately,
WaitIdle,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum FileChangeKind {
Added,
Modified,
Deleted,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FileChange {
pub kind: FileChangeKind,
pub path: String,
}
#[cfg(test)]
mod tests {
#![allow(
clippy::unwrap_used,
clippy::expect_used,
clippy::panic,
clippy::field_reassign_with_default
)]
use super::*;
use serde_json::json;
#[test]
fn test_tool_call_construction() {
let tc = ToolCall {
id: "call_1".to_string(),
name: "read_file".to_string(),
args: json!({"path": "/tmp/foo"}),
canonical_path: None,
};
assert_eq!(tc.name, "read_file");
assert_eq!(tc.args["path"], "/tmp/foo");
assert_eq!(tc.id, "call_1");
assert_eq!(tc.canonical_path, None);
}
#[test]
fn test_tool_call_serialization() {
let json_data = r#"{"id":"call_1","name":"read_file","args":{"path":"/tmp/foo"}}"#;
let tc: ToolCall = serde_json::from_str(json_data).unwrap();
assert_eq!(tc.name, "read_file");
assert_eq!(tc.args["path"], "/tmp/foo");
assert_eq!(tc.id, "call_1");
assert_eq!(tc.canonical_path, None);
}
#[test]
fn test_tool_result_success() {
let tr = ToolResult {
name: "sum_tool".to_string(),
id: Some("call_1".to_string()),
result: Some(json!(42)),
error: None,
};
assert_eq!(tr.name, "sum_tool");
assert_eq!(tr.result.unwrap(), 42);
assert!(tr.error.is_none());
assert_eq!(tr.id.unwrap(), "call_1");
}
#[test]
fn test_tool_result_error() {
let tr = ToolResult {
name: "bad_tool".to_string(),
id: None,
result: None,
error: Some("kaboom".to_string()),
};
assert_eq!(tr.name, "bad_tool");
assert!(tr.result.is_none());
assert_eq!(tr.error.unwrap(), "kaboom");
assert!(tr.id.is_none());
}
#[test]
fn test_tool_result_mutability() {
let mut tr = ToolResult {
name: "tool".to_string(),
id: None,
result: None,
error: None,
};
tr.result = Some(json!("updated"));
assert_eq!(tr.result.unwrap(), "updated");
}
#[test]
fn test_step_defaults() {
let step = Step::default();
assert_eq!(step.id, "");
assert_eq!(step.step_index, 0);
assert!(matches!(step.r#type, StepType::Unknown));
assert!(matches!(step.status, StepStatus::Unknown));
assert!(matches!(step.source, StepSource::Unknown));
assert_eq!(step.content, "");
assert!(step.tool_calls.is_empty());
assert_eq!(step.error, "");
}
#[test]
fn test_step_mutability() {
let mut step = Step::default();
step.content = "goodbye".to_string();
assert_eq!(step.content, "goodbye");
}
#[test]
fn test_hook_result_defaults() {
let hr = HookResult::default();
assert!(!hr.allow); assert_eq!(hr.message, "");
}
#[test]
fn test_question_response_defaults() {
let qr = QuestionResponse {
selected_option_ids: None,
freeform_response: String::new(),
skipped: false,
};
assert!(qr.selected_option_ids.is_none());
assert_eq!(qr.freeform_response, "");
assert!(!qr.skipped);
}
#[test]
fn test_question_response_skipped() {
let qr = QuestionResponse {
selected_option_ids: None,
freeform_response: String::new(),
skipped: true,
};
assert!(qr.skipped);
}
#[test]
fn test_gemini_config_defaults() {
let config = GeminiConfig::default();
assert!(config.api_key.is_none());
assert!(!config.vertex);
assert!(config.project.is_none());
assert!(config.location.is_none());
assert_eq!(config.models.default.name, DEFAULT_MODEL);
assert!(config.models.default.generation.thinking_level.is_none());
assert!(config.enable_google_search.is_none());
assert!(config.enable_url_context.is_none());
}
#[test]
fn test_thinking_level_serialization() {
let level = ThinkingLevel::Low;
let json_str = serde_json::to_string(&level).unwrap();
assert_eq!(json_str, "\"low\"");
}
#[test]
fn test_capabilities_config_defaults() {
let config = CapabilitiesConfig::default();
assert!(config.enabled_tools.is_none());
assert!(config.disabled_tools.is_none());
assert!(config.compaction_threshold.is_none());
}
#[test]
fn test_content_from_str() {
let content: Content = "hello".into();
assert_eq!(content.as_text(), Some("hello"));
}
#[test]
fn test_content_from_string() {
let content: Content = String::from("world").into();
assert_eq!(content.as_text(), Some("world"));
}
#[test]
fn test_content_text_constructor() {
let content = Content::text("test");
assert_eq!(content.as_text(), Some("test"));
}
#[test]
fn test_content_media_has_no_text() {
let media = Media {
data: vec![0xFF, 0xD8],
mime_type: MimeType::Image(ImageMime::Jpeg),
description: None,
};
let content = Content::media(media);
assert_eq!(content.as_text(), None);
}
#[test]
fn test_mime_from_extension_images() {
assert_eq!(
mime_from_extension("png"),
Some(MimeType::Image(ImageMime::Png))
);
assert_eq!(
mime_from_extension("jpg"),
Some(MimeType::Image(ImageMime::Jpeg))
);
assert_eq!(
mime_from_extension("jpeg"),
Some(MimeType::Image(ImageMime::Jpeg))
);
assert_eq!(
mime_from_extension("webp"),
Some(MimeType::Image(ImageMime::Webp))
);
assert_eq!(
mime_from_extension("bmp"),
Some(MimeType::Image(ImageMime::Bmp))
);
}
#[test]
fn test_mime_from_extension_documents() {
assert_eq!(
mime_from_extension("pdf"),
Some(MimeType::Document(DocumentMime::Pdf))
);
assert_eq!(
mime_from_extension("json"),
Some(MimeType::Document(DocumentMime::Json))
);
assert_eq!(
mime_from_extension("txt"),
Some(MimeType::Document(DocumentMime::PlainText))
);
assert_eq!(
mime_from_extension("md"),
Some(MimeType::Document(DocumentMime::PlainText))
);
}
#[test]
fn test_mime_from_extension_audio() {
assert_eq!(
mime_from_extension("mp3"),
Some(MimeType::Audio(AudioMime::Mp3))
);
assert_eq!(
mime_from_extension("wav"),
Some(MimeType::Audio(AudioMime::Wav))
);
assert_eq!(
mime_from_extension("flac"),
Some(MimeType::Audio(AudioMime::Flac))
);
}
#[test]
fn test_mime_from_extension_video() {
assert_eq!(
mime_from_extension("mp4"),
Some(MimeType::Video(VideoMime::Mp4))
);
assert_eq!(
mime_from_extension("webm"),
Some(MimeType::Video(VideoMime::Webm))
);
assert_eq!(
mime_from_extension("mov"),
Some(MimeType::Video(VideoMime::Quicktime))
);
}
#[test]
fn test_mime_from_extension_unsupported() {
assert_eq!(mime_from_extension("xyz"), None);
assert_eq!(mime_from_extension(""), None);
}
#[test]
fn test_mime_type_display() {
let m = MimeType::Image(ImageMime::Png);
assert_eq!(m.to_string(), "image/png");
assert_eq!(m.as_str(), "image/png");
}
#[test]
fn test_content_from_file_missing() {
let result = Content::from_file("/nonexistent/file.png", None);
assert!(result.is_err());
}
#[test]
fn test_content_from_file_unsupported_ext() {
let dir = std::env::temp_dir();
let path = dir.join("test_antigravity.xyz");
std::fs::write(&path, b"data").unwrap();
let result = Content::from_file(&path, None);
assert!(result.is_err());
assert!(result.unwrap_err().contains("Unsupported"));
std::fs::remove_file(&path).unwrap();
}
#[test]
fn test_content_from_file_success() {
let dir = std::env::temp_dir();
let path = dir.join("test_antigravity.txt");
std::fs::write(&path, b"hello world").unwrap();
let result = Content::from_file(&path, Some("test doc"));
assert!(result.is_ok());
let content = result.unwrap();
assert_eq!(content.as_text(), None);
std::fs::remove_file(&path).unwrap();
}
#[test]
fn test_trigger_delivery_serialization() {
let d = TriggerDelivery::SendImmediately;
let json_str = serde_json::to_string(&d).unwrap();
assert_eq!(json_str, "\"send_immediately\"");
}
#[test]
fn test_file_change_kind_serialization() {
let k = FileChangeKind::Modified;
let json_str = serde_json::to_string(&k).unwrap();
assert_eq!(json_str, "\"modified\"");
}
#[test]
fn test_file_change_construction() {
let fc = FileChange {
kind: FileChangeKind::Added,
path: "/tmp/new_file.txt".to_string(),
};
assert!(matches!(fc.kind, FileChangeKind::Added));
assert_eq!(fc.path, "/tmp/new_file.txt");
}
}