Fix system sleep prevention and add comprehensive test suite
- Fixed terminal control preventing system sleep by improving rustyline configuration and adding proper cleanup - Added signal handling for graceful termination and terminal state reset - Implemented comprehensive test suite with 58 unit and integration tests - Added testing dependencies: tempfile, mockall, tokio-test, serial_test - Created proper Drop implementation for InputHandler to ensure terminal cleanup - Enhanced exit handling in both normal exit and /exit command 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
304
src/config.rs
304
src/config.rs
@@ -155,7 +155,7 @@ impl Config {
|
||||
Ok(home.join(".config").join("gpt-cli-rust").join("config.toml"))
|
||||
}
|
||||
|
||||
fn apply_env_overrides(&mut self) -> Result<()> {
|
||||
pub fn apply_env_overrides(&mut self) -> Result<()> {
|
||||
// Override API URLs
|
||||
if let Ok(openai_base_url) = env::var("OPENAI_BASE_URL") {
|
||||
self.api.openai_base_url = openai_base_url;
|
||||
@@ -244,4 +244,306 @@ impl Config {
|
||||
self.defaults.default_session = session_name;
|
||||
self.save()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::env;
|
||||
use tempfile::{NamedTempFile, TempDir};
|
||||
|
||||
fn create_test_config() -> Config {
|
||||
Config {
|
||||
api: ApiConfig {
|
||||
openai_base_url: "https://test-openai.com".to_string(),
|
||||
anthropic_base_url: "https://test-anthropic.com".to_string(),
|
||||
anthropic_version: "2023-06-01".to_string(),
|
||||
request_timeout_seconds: 60,
|
||||
max_retries: 2,
|
||||
},
|
||||
defaults: DefaultsConfig {
|
||||
model: "test-model".to_string(),
|
||||
reasoning_effort: "low".to_string(),
|
||||
enable_web_search: false,
|
||||
enable_reasoning_summary: true,
|
||||
default_session: "test-session".to_string(),
|
||||
},
|
||||
limits: LimitsConfig {
|
||||
max_tokens_anthropic: 2048,
|
||||
max_conversation_history: 50,
|
||||
max_sessions_to_list: 25,
|
||||
},
|
||||
session: SessionConfig {
|
||||
sessions_dir_name: ".test_sessions".to_string(),
|
||||
file_extension: "json".to_string(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_config_defaults() {
|
||||
let config = Config::default();
|
||||
|
||||
assert_eq!(config.api.openai_base_url, "https://api.openai.com/v1");
|
||||
assert_eq!(config.api.anthropic_base_url, "https://api.anthropic.com/v1");
|
||||
assert_eq!(config.api.anthropic_version, "2023-06-01");
|
||||
assert_eq!(config.api.request_timeout_seconds, 120);
|
||||
assert_eq!(config.api.max_retries, 3);
|
||||
|
||||
assert_eq!(config.defaults.model, "gpt-5");
|
||||
assert_eq!(config.defaults.reasoning_effort, "medium");
|
||||
assert!(config.defaults.enable_web_search);
|
||||
assert!(!config.defaults.enable_reasoning_summary);
|
||||
assert_eq!(config.defaults.default_session, "default");
|
||||
|
||||
assert_eq!(config.limits.max_tokens_anthropic, 4096);
|
||||
assert_eq!(config.limits.max_conversation_history, 100);
|
||||
assert_eq!(config.limits.max_sessions_to_list, 50);
|
||||
|
||||
assert_eq!(config.session.sessions_dir_name, ".chat_cli_sessions");
|
||||
assert_eq!(config.session.file_extension, "json");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_config_serialization() {
|
||||
let config = create_test_config();
|
||||
|
||||
let toml_str = toml::to_string_pretty(&config).unwrap();
|
||||
let deserialized: Config = toml::from_str(&toml_str).unwrap();
|
||||
|
||||
assert_eq!(config.api.openai_base_url, deserialized.api.openai_base_url);
|
||||
assert_eq!(config.defaults.model, deserialized.defaults.model);
|
||||
assert_eq!(config.limits.max_tokens_anthropic, deserialized.limits.max_tokens_anthropic);
|
||||
assert_eq!(config.session.sessions_dir_name, deserialized.session.sessions_dir_name);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_config_save_and_load() {
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
let config_path = temp_dir.path().join("config.toml");
|
||||
|
||||
// Mock the config file path
|
||||
let original_config = create_test_config();
|
||||
let toml_content = toml::to_string_pretty(&original_config).unwrap();
|
||||
std::fs::write(&config_path, toml_content).unwrap();
|
||||
|
||||
// Test loading from file content directly since we can't easily mock the config_file_path
|
||||
let file_content = std::fs::read_to_string(&config_path).unwrap();
|
||||
let loaded_config: Config = toml::from_str(&file_content).unwrap();
|
||||
|
||||
assert_eq!(original_config.api.openai_base_url, loaded_config.api.openai_base_url);
|
||||
assert_eq!(original_config.defaults.model, loaded_config.defaults.model);
|
||||
assert_eq!(original_config.limits.max_tokens_anthropic, loaded_config.limits.max_tokens_anthropic);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_env_variable_validation_with_both_keys() {
|
||||
env::set_var("OPENAI_API_KEY", "test-openai-key");
|
||||
env::set_var("ANTHROPIC_API_KEY", "test-anthropic-key");
|
||||
env::set_var("OPENAI_BASE_URL", "https://custom-openai.com");
|
||||
env::set_var("DEFAULT_MODEL", "custom-model");
|
||||
|
||||
let env_vars = Config::validate_env_variables().unwrap();
|
||||
|
||||
assert_eq!(env_vars.openai_api_key, Some("test-openai-key".to_string()));
|
||||
assert_eq!(env_vars.anthropic_api_key, Some("test-anthropic-key".to_string()));
|
||||
assert_eq!(env_vars.openai_base_url, Some("https://custom-openai.com".to_string()));
|
||||
assert_eq!(env_vars.default_model, Some("custom-model".to_string()));
|
||||
|
||||
// Clean up
|
||||
env::remove_var("OPENAI_API_KEY");
|
||||
env::remove_var("ANTHROPIC_API_KEY");
|
||||
env::remove_var("OPENAI_BASE_URL");
|
||||
env::remove_var("DEFAULT_MODEL");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_env_variable_validation_with_only_openai() {
|
||||
// Store current values to restore later
|
||||
let original_openai = env::var("OPENAI_API_KEY").ok();
|
||||
let original_anthropic = env::var("ANTHROPIC_API_KEY").ok();
|
||||
|
||||
// Ensure anthropic key is not set
|
||||
env::remove_var("ANTHROPIC_API_KEY");
|
||||
env::set_var("OPENAI_API_KEY", "test-openai-key-only");
|
||||
|
||||
let env_vars = Config::validate_env_variables().unwrap();
|
||||
|
||||
assert_eq!(env_vars.openai_api_key, Some("test-openai-key-only".to_string()));
|
||||
assert_eq!(env_vars.anthropic_api_key, None);
|
||||
|
||||
// Restore original values if they existed
|
||||
env::remove_var("OPENAI_API_KEY");
|
||||
env::remove_var("ANTHROPIC_API_KEY");
|
||||
if let Some(value) = original_openai {
|
||||
env::set_var("OPENAI_API_KEY", value);
|
||||
}
|
||||
if let Some(value) = original_anthropic {
|
||||
env::set_var("ANTHROPIC_API_KEY", value);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_env_variable_validation_with_only_anthropic() {
|
||||
// Ensure openai key is not set
|
||||
env::remove_var("OPENAI_API_KEY");
|
||||
env::set_var("ANTHROPIC_API_KEY", "test-anthropic-key-only");
|
||||
|
||||
let env_vars = Config::validate_env_variables().unwrap();
|
||||
|
||||
assert_eq!(env_vars.openai_api_key, None);
|
||||
assert_eq!(env_vars.anthropic_api_key, Some("test-anthropic-key-only".to_string()));
|
||||
|
||||
// Clean up
|
||||
env::remove_var("ANTHROPIC_API_KEY");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_env_variable_validation_with_no_keys() {
|
||||
// Store current values to restore later
|
||||
let original_openai = env::var("OPENAI_API_KEY").ok();
|
||||
let original_anthropic = env::var("ANTHROPIC_API_KEY").ok();
|
||||
|
||||
// Ensure both keys are not set
|
||||
env::remove_var("OPENAI_API_KEY");
|
||||
env::remove_var("ANTHROPIC_API_KEY");
|
||||
|
||||
let result = Config::validate_env_variables();
|
||||
|
||||
assert!(result.is_err());
|
||||
assert!(result.unwrap_err().to_string().contains("At least one API key must be set"));
|
||||
|
||||
// Restore original values if they existed
|
||||
if let Some(value) = original_openai {
|
||||
env::set_var("OPENAI_API_KEY", value);
|
||||
}
|
||||
if let Some(value) = original_anthropic {
|
||||
env::set_var("ANTHROPIC_API_KEY", value);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_model_availability_validation_openai() {
|
||||
env::set_var("OPENAI_API_KEY", "test-key");
|
||||
env::remove_var("ANTHROPIC_API_KEY");
|
||||
|
||||
let config = Config::default();
|
||||
let env_vars = EnvVariables {
|
||||
openai_api_key: Some("test-key".to_string()),
|
||||
anthropic_api_key: None,
|
||||
openai_base_url: None,
|
||||
default_model: None,
|
||||
};
|
||||
|
||||
// Should succeed for OpenAI model
|
||||
assert!(config.validate_model_availability(&env_vars, "gpt-4").is_ok());
|
||||
|
||||
// Should fail for Anthropic model without key
|
||||
assert!(config.validate_model_availability(&env_vars, "claude-sonnet-4-20250514").is_err());
|
||||
|
||||
env::remove_var("OPENAI_API_KEY");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_model_availability_validation_anthropic() {
|
||||
env::remove_var("OPENAI_API_KEY");
|
||||
env::set_var("ANTHROPIC_API_KEY", "test-key");
|
||||
|
||||
let config = Config::default();
|
||||
let env_vars = EnvVariables {
|
||||
openai_api_key: None,
|
||||
anthropic_api_key: Some("test-key".to_string()),
|
||||
openai_base_url: None,
|
||||
default_model: None,
|
||||
};
|
||||
|
||||
// Should succeed for Anthropic model
|
||||
assert!(config.validate_model_availability(&env_vars, "claude-sonnet-4-20250514").is_ok());
|
||||
|
||||
// Should fail for OpenAI model without key
|
||||
assert!(config.validate_model_availability(&env_vars, "gpt-4").is_err());
|
||||
|
||||
env::remove_var("ANTHROPIC_API_KEY");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_apply_env_overrides() {
|
||||
env::set_var("OPENAI_BASE_URL", "https://override-openai.com");
|
||||
env::set_var("DEFAULT_MODEL", "override-model");
|
||||
|
||||
let mut config = Config::default();
|
||||
config.apply_env_overrides().unwrap();
|
||||
|
||||
assert_eq!(config.api.openai_base_url, "https://override-openai.com");
|
||||
assert_eq!(config.defaults.model, "override-model");
|
||||
|
||||
// Clean up
|
||||
env::remove_var("OPENAI_BASE_URL");
|
||||
env::remove_var("DEFAULT_MODEL");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_default_session_name_function() {
|
||||
assert_eq!(default_session_name(), "default");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_set_default_session() {
|
||||
let _temp_file = NamedTempFile::new().unwrap();
|
||||
let mut config = create_test_config();
|
||||
|
||||
// Test the mutation
|
||||
assert_eq!(config.defaults.default_session, "test-session");
|
||||
config.defaults.default_session = "new-session".to_string();
|
||||
assert_eq!(config.defaults.default_session, "new-session");
|
||||
|
||||
// Note: We can't easily test the full set_default_session method
|
||||
// without mocking the file system, but we've tested the core logic
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_config_file_path() {
|
||||
let path = Config::config_file_path().unwrap();
|
||||
assert!(path.to_string_lossy().contains(".config"));
|
||||
assert!(path.to_string_lossy().contains("gpt-cli-rust"));
|
||||
assert!(path.to_string_lossy().contains("config.toml"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invalid_toml_parsing() {
|
||||
let invalid_toml = "this is not valid toml content [[[[";
|
||||
let result: Result<Config, _> = toml::from_str(invalid_toml);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_config_with_missing_optional_fields() {
|
||||
let minimal_toml = r#"
|
||||
[api]
|
||||
openai_base_url = "https://api.openai.com/v1"
|
||||
anthropic_base_url = "https://api.anthropic.com/v1"
|
||||
anthropic_version = "2023-06-01"
|
||||
request_timeout_seconds = 120
|
||||
max_retries = 3
|
||||
|
||||
[defaults]
|
||||
model = "gpt-4"
|
||||
reasoning_effort = "medium"
|
||||
enable_web_search = true
|
||||
enable_reasoning_summary = false
|
||||
# default_session field is optional due to serde default
|
||||
|
||||
[limits]
|
||||
max_tokens_anthropic = 4096
|
||||
max_conversation_history = 100
|
||||
max_sessions_to_list = 50
|
||||
|
||||
[session]
|
||||
sessions_dir_name = ".chat_cli_sessions"
|
||||
file_extension = "json"
|
||||
"#;
|
||||
|
||||
let config: Config = toml::from_str(minimal_toml).unwrap();
|
||||
assert_eq!(config.defaults.default_session, "default"); // Should use the default value
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user