use std::path::Path;
use tempfile::TempDir;
use tokensave::agents::*;
#[test]
fn test_get_all_integrations() {
let all = all_integrations();
assert_eq!(all.len(), 9);
}
#[test]
fn test_available_integrations() {
let ids = available_integrations();
assert!(ids.contains(&"claude"));
assert!(ids.contains(&"copilot"));
assert!(ids.contains(&"codex"));
assert!(ids.contains(&"gemini"));
assert!(ids.contains(&"opencode"));
assert!(ids.contains(&"cursor"));
assert!(ids.contains(&"zed"));
assert!(ids.contains(&"cline"));
assert!(ids.contains(&"roo-code"));
assert_eq!(ids.len(), 9);
}
#[test]
fn test_get_integration_valid() {
for id in &[
"claude", "opencode", "codex", "gemini", "copilot", "cursor", "zed", "cline", "roo-code",
] {
let agent = get_integration(id).unwrap();
assert_eq!(agent.id(), *id);
}
}
#[test]
fn test_get_integration_invalid() {
assert!(get_integration("nonexistent").is_err());
assert!(get_integration("").is_err());
assert!(get_integration("CLAUDE").is_err()); }
#[test]
fn test_agent_names_and_ids() {
for agent in all_integrations() {
assert!(!agent.name().is_empty(), "agent name should not be empty");
assert!(!agent.id().is_empty(), "agent id should not be empty");
}
}
#[test]
fn test_agent_names_are_human_readable() {
let expected_names: Vec<(&str, &str)> = vec![
("claude", "Claude Code"),
("copilot", "GitHub Copilot"),
("codex", "Codex CLI"),
("gemini", "Gemini CLI"),
("opencode", "OpenCode"),
("cursor", "Cursor"),
("zed", "Zed"),
("cline", "Cline"),
("roo-code", "Roo Code"),
];
for (id, expected_name) in expected_names {
let agent = get_integration(id).unwrap();
assert_eq!(agent.name(), expected_name, "name mismatch for agent {id}");
}
}
fn make_install_ctx(home: &Path) -> InstallContext {
InstallContext {
home: home.to_path_buf(),
tokensave_bin: "/usr/local/bin/tokensave".to_string(),
tool_permissions: EXPECTED_TOOL_PERMS,
}
}
#[test]
fn test_claude_install_creates_config() {
let dir = TempDir::new().unwrap();
let home = dir.path();
let ctx = make_install_ctx(home);
ClaudeIntegration.install(&ctx).unwrap();
let claude_json = home.join(".claude.json");
assert!(claude_json.exists(), "~/.claude.json should exist after install");
let content: serde_json::Value =
serde_json::from_str(&std::fs::read_to_string(&claude_json).unwrap()).unwrap();
assert!(
content.get("mcpServers").is_some(),
"mcpServers key should exist"
);
assert!(
content["mcpServers"]["tokensave"].is_object(),
"mcpServers.tokensave should be an object"
);
let args = content["mcpServers"]["tokensave"]["args"].as_array().unwrap();
assert!(args.iter().any(|v| v.as_str() == Some("serve")));
let settings_path = home.join(".claude/settings.json");
assert!(settings_path.exists(), "settings.json should exist after install");
let settings: serde_json::Value =
serde_json::from_str(&std::fs::read_to_string(&settings_path).unwrap()).unwrap();
assert!(
settings["hooks"]["PreToolUse"].is_array(),
"PreToolUse hook should be an array"
);
assert!(
settings["permissions"]["allow"].is_array(),
"permissions.allow should be an array"
);
let claude_md = home.join(".claude/CLAUDE.md");
assert!(claude_md.exists(), "CLAUDE.md should exist after install");
let md_content = std::fs::read_to_string(&claude_md).unwrap();
assert!(md_content.contains("tokensave"), "CLAUDE.md should mention tokensave");
}
#[test]
fn test_gemini_install_creates_config() {
let dir = TempDir::new().unwrap();
let home = dir.path();
let ctx = make_install_ctx(home);
GeminiIntegration.install(&ctx).unwrap();
let settings_path = home.join(".gemini/settings.json");
assert!(settings_path.exists(), "settings.json should exist after install");
let content: serde_json::Value =
serde_json::from_str(&std::fs::read_to_string(&settings_path).unwrap()).unwrap();
assert!(
content["mcpServers"]["tokensave"].is_object(),
"mcpServers.tokensave should exist"
);
assert_eq!(
content["mcpServers"]["tokensave"]["trust"],
serde_json::json!(true),
"gemini should have trust: true"
);
let gemini_md = home.join(".gemini/GEMINI.md");
assert!(gemini_md.exists(), "GEMINI.md should exist after install");
let md_content = std::fs::read_to_string(&gemini_md).unwrap();
assert!(md_content.contains("tokensave"));
}
#[test]
fn test_codex_install_creates_config() {
let dir = TempDir::new().unwrap();
let home = dir.path();
let ctx = make_install_ctx(home);
CodexIntegration.install(&ctx).unwrap();
let config_path = home.join(".codex/config.toml");
assert!(config_path.exists(), "config.toml should exist after install");
let content = std::fs::read_to_string(&config_path).unwrap();
assert!(
content.contains("[mcp_servers.tokensave]"),
"config.toml should contain [mcp_servers.tokensave]"
);
assert!(
content.contains("\"serve\""),
"config.toml should contain \"serve\" in args"
);
let agents_md = home.join(".codex/AGENTS.md");
assert!(agents_md.exists(), "AGENTS.md should exist after install");
let md_content = std::fs::read_to_string(&agents_md).unwrap();
assert!(md_content.contains("tokensave"));
}
#[test]
fn test_cursor_install_creates_config() {
let dir = TempDir::new().unwrap();
let home = dir.path();
let ctx = make_install_ctx(home);
CursorIntegration.install(&ctx).unwrap();
let mcp_path = home.join(".cursor/mcp.json");
assert!(mcp_path.exists(), "mcp.json should exist after install");
let content: serde_json::Value =
serde_json::from_str(&std::fs::read_to_string(&mcp_path).unwrap()).unwrap();
assert!(content["mcpServers"]["tokensave"].is_object());
}
#[test]
fn test_opencode_install_creates_config() {
let dir = TempDir::new().unwrap();
let home = dir.path();
let ctx = make_install_ctx(home);
OpenCodeIntegration.install(&ctx).unwrap();
let config_path = home.join(".config/opencode/opencode.json");
assert!(config_path.exists(), "opencode.json should exist after install");
let content: serde_json::Value =
serde_json::from_str(&std::fs::read_to_string(&config_path).unwrap()).unwrap();
assert!(content["mcp"]["tokensave"].is_object());
}
#[test]
fn test_zed_install_creates_config() {
let dir = TempDir::new().unwrap();
let home = dir.path();
let ctx = make_install_ctx(home);
ZedIntegration.install(&ctx).unwrap();
#[cfg(target_os = "macos")]
let settings_path = home.join("Library/Application Support/Zed/settings.json");
#[cfg(not(target_os = "macos"))]
let settings_path = home.join(".config/zed/settings.json");
assert!(settings_path.exists(), "Zed settings.json should exist after install");
let content: serde_json::Value =
serde_json::from_str(&std::fs::read_to_string(&settings_path).unwrap()).unwrap();
assert!(content["context_servers"]["tokensave"].is_object());
}
#[test]
fn test_cline_install_creates_config() {
let dir = TempDir::new().unwrap();
let home = dir.path();
let ctx = make_install_ctx(home);
ClineIntegration.install(&ctx).unwrap();
#[cfg(target_os = "macos")]
let settings_path = home.join("Library/Application Support/Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json");
#[cfg(target_os = "linux")]
let settings_path = home.join(".config/Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json");
#[cfg(target_os = "windows")]
let settings_path = home.join("AppData/Roaming/Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json");
#[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
let settings_path = home.join(".config/Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json");
assert!(settings_path.exists(), "Cline settings should exist after install");
let content: serde_json::Value =
serde_json::from_str(&std::fs::read_to_string(&settings_path).unwrap()).unwrap();
assert!(content["mcpServers"]["tokensave"].is_object());
}
#[test]
fn test_roo_code_install_creates_config() {
let dir = TempDir::new().unwrap();
let home = dir.path();
let ctx = make_install_ctx(home);
RooCodeIntegration.install(&ctx).unwrap();
#[cfg(target_os = "macos")]
let settings_path = home.join("Library/Application Support/Code/User/globalStorage/rooveterinaryinc.roo-cline/settings/cline_mcp_settings.json");
#[cfg(target_os = "linux")]
let settings_path = home.join(".config/Code/User/globalStorage/rooveterinaryinc.roo-cline/settings/cline_mcp_settings.json");
#[cfg(target_os = "windows")]
let settings_path = home.join("AppData/Roaming/Code/User/globalStorage/rooveterinaryinc.roo-cline/settings/cline_mcp_settings.json");
#[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
let settings_path = home.join(".config/Code/User/globalStorage/rooveterinaryinc.roo-cline/settings/cline_mcp_settings.json");
assert!(settings_path.exists(), "Roo Code settings should exist after install");
let content: serde_json::Value =
serde_json::from_str(&std::fs::read_to_string(&settings_path).unwrap()).unwrap();
assert!(content["mcpServers"]["tokensave"].is_object());
}
#[test]
fn test_copilot_install_creates_config() {
let dir = TempDir::new().unwrap();
let home = dir.path();
let ctx = make_install_ctx(home);
CopilotIntegration.install(&ctx).unwrap();
#[cfg(target_os = "macos")]
let vscode_settings = home.join("Library/Application Support/Code/User/settings.json");
#[cfg(target_os = "linux")]
let vscode_settings = home.join(".config/Code/User/settings.json");
#[cfg(target_os = "windows")]
let vscode_settings = home.join("AppData/Roaming/Code/User/settings.json");
#[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
let vscode_settings = home.join(".config/Code/User/settings.json");
assert!(vscode_settings.exists(), "VS Code settings.json should exist");
let content: serde_json::Value =
serde_json::from_str(&std::fs::read_to_string(&vscode_settings).unwrap()).unwrap();
assert!(content["mcp"]["servers"]["tokensave"].is_object());
let cli_config = home.join(".copilot/mcp-config.json");
assert!(cli_config.exists(), "Copilot CLI config should exist");
let cli_content: serde_json::Value =
serde_json::from_str(&std::fs::read_to_string(&cli_config).unwrap()).unwrap();
assert!(cli_content["mcpServers"]["tokensave"].is_object());
}
#[test]
fn test_claude_install_then_uninstall() {
let dir = TempDir::new().unwrap();
let home = dir.path();
let ctx = make_install_ctx(home);
ClaudeIntegration.install(&ctx).unwrap();
assert!(home.join(".claude.json").exists());
ClaudeIntegration.uninstall(&ctx).unwrap();
if home.join(".claude.json").exists() {
let content: serde_json::Value = serde_json::from_str(
&std::fs::read_to_string(home.join(".claude.json")).unwrap(),
)
.unwrap();
let has_tokensave = content
.get("mcpServers")
.and_then(|v| v.get("tokensave"))
.is_some();
assert!(!has_tokensave, "tokensave should be removed from .claude.json after uninstall");
}
}
#[test]
fn test_gemini_install_then_uninstall() {
let dir = TempDir::new().unwrap();
let home = dir.path();
let ctx = make_install_ctx(home);
GeminiIntegration.install(&ctx).unwrap();
let settings_path = home.join(".gemini/settings.json");
assert!(settings_path.exists());
GeminiIntegration.uninstall(&ctx).unwrap();
if settings_path.exists() {
let content: serde_json::Value =
serde_json::from_str(&std::fs::read_to_string(&settings_path).unwrap()).unwrap();
let has_tokensave = content
.get("mcpServers")
.and_then(|v| v.get("tokensave"))
.is_some();
assert!(!has_tokensave, "tokensave should be removed from settings.json");
}
let gemini_md = home.join(".gemini/GEMINI.md");
if gemini_md.exists() {
let content = std::fs::read_to_string(&gemini_md).unwrap();
assert!(
!content.contains("## Prefer tokensave MCP tools"),
"GEMINI.md should not contain tokensave rules after uninstall"
);
}
}
#[test]
fn test_codex_install_then_uninstall() {
let dir = TempDir::new().unwrap();
let home = dir.path();
let ctx = make_install_ctx(home);
CodexIntegration.install(&ctx).unwrap();
assert!(home.join(".codex/config.toml").exists());
CodexIntegration.uninstall(&ctx).unwrap();
let agents_md = home.join(".codex/AGENTS.md");
if agents_md.exists() {
let content = std::fs::read_to_string(&agents_md).unwrap();
assert!(
!content.contains("## Prefer tokensave MCP tools"),
"AGENTS.md should not have tokensave rules after uninstall"
);
}
}
#[test]
fn test_cursor_install_then_uninstall() {
let dir = TempDir::new().unwrap();
let home = dir.path();
let ctx = make_install_ctx(home);
CursorIntegration.install(&ctx).unwrap();
let mcp_path = home.join(".cursor/mcp.json");
assert!(mcp_path.exists());
CursorIntegration.uninstall(&ctx).unwrap();
if mcp_path.exists() {
let content: serde_json::Value =
serde_json::from_str(&std::fs::read_to_string(&mcp_path).unwrap()).unwrap();
let has_tokensave = content
.get("mcpServers")
.and_then(|v| v.get("tokensave"))
.is_some();
assert!(!has_tokensave, "tokensave should be removed from mcp.json");
}
}
#[test]
fn test_copilot_install_then_uninstall() {
let dir = TempDir::new().unwrap();
let home = dir.path();
let ctx = make_install_ctx(home);
CopilotIntegration.install(&ctx).unwrap();
CopilotIntegration.uninstall(&ctx).unwrap();
let cli_config = home.join(".copilot/mcp-config.json");
if cli_config.exists() {
let content: serde_json::Value =
serde_json::from_str(&std::fs::read_to_string(&cli_config).unwrap()).unwrap();
let has_tokensave = content
.get("mcpServers")
.and_then(|v| v.get("tokensave"))
.is_some();
assert!(!has_tokensave);
}
}
fn make_install_ctx_with_real_bin(home: &Path) -> InstallContext {
let bin_dir = home.join("bin");
std::fs::create_dir_all(&bin_dir).unwrap();
let bin_path = bin_dir.join("tokensave");
std::fs::write(&bin_path, "#!/bin/sh\n").unwrap();
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(&bin_path, std::fs::Permissions::from_mode(0o755)).unwrap();
}
InstallContext {
home: home.to_path_buf(),
tokensave_bin: bin_path.to_string_lossy().to_string(),
tool_permissions: EXPECTED_TOOL_PERMS,
}
}
#[test]
fn test_healthcheck_claude_clean_install() {
let dir = TempDir::new().unwrap();
let home = dir.path();
let ctx = make_install_ctx_with_real_bin(home);
ClaudeIntegration.install(&ctx).unwrap();
let mut dc = DoctorCounters::new();
let hctx = HealthcheckContext {
home: home.to_path_buf(),
project_path: home.to_path_buf(),
};
ClaudeIntegration.healthcheck(&mut dc, &hctx);
assert_eq!(dc.issues, 0, "clean Claude install should have no issues");
}
#[test]
fn test_healthcheck_gemini_clean_install() {
let dir = TempDir::new().unwrap();
let home = dir.path();
let ctx = make_install_ctx(home);
GeminiIntegration.install(&ctx).unwrap();
let mut dc = DoctorCounters::new();
let hctx = HealthcheckContext {
home: home.to_path_buf(),
project_path: home.to_path_buf(),
};
GeminiIntegration.healthcheck(&mut dc, &hctx);
assert_eq!(dc.issues, 0, "clean Gemini install should have no issues");
}
#[test]
fn test_healthcheck_codex_after_install() {
let dir = TempDir::new().unwrap();
let home = dir.path();
let ctx = make_install_ctx(home);
CodexIntegration.install(&ctx).unwrap();
let mut dc = DoctorCounters::new();
let hctx = HealthcheckContext {
home: home.to_path_buf(),
project_path: home.to_path_buf(),
};
CodexIntegration.healthcheck(&mut dc, &hctx);
assert_eq!(
dc.issues, 1,
"Codex healthcheck reports MCP not found due to toml::Value parse limitation"
);
}
#[test]
fn test_healthcheck_cursor_clean_install() {
let dir = TempDir::new().unwrap();
let home = dir.path();
let ctx = make_install_ctx(home);
CursorIntegration.install(&ctx).unwrap();
let mut dc = DoctorCounters::new();
let hctx = HealthcheckContext {
home: home.to_path_buf(),
project_path: home.to_path_buf(),
};
CursorIntegration.healthcheck(&mut dc, &hctx);
assert_eq!(dc.issues, 0, "clean Cursor install should have no issues");
}
#[test]
fn test_healthcheck_opencode_clean_install() {
let dir = TempDir::new().unwrap();
let home = dir.path();
let ctx = make_install_ctx(home);
OpenCodeIntegration.install(&ctx).unwrap();
let mut dc = DoctorCounters::new();
let hctx = HealthcheckContext {
home: home.to_path_buf(),
project_path: home.to_path_buf(),
};
OpenCodeIntegration.healthcheck(&mut dc, &hctx);
assert_eq!(dc.issues, 0, "clean OpenCode install should have no issues");
}
#[test]
fn test_healthcheck_no_install_warns() {
let dir = TempDir::new().unwrap();
let home = dir.path();
let mut dc = DoctorCounters::new();
let hctx = HealthcheckContext {
home: home.to_path_buf(),
project_path: home.to_path_buf(),
};
ClaudeIntegration.healthcheck(&mut dc, &hctx);
assert!(
dc.issues > 0 || dc.warnings > 0,
"healthcheck on empty dir should report issues or warnings"
);
}
#[test]
fn test_doctor_counters() {
let mut dc = DoctorCounters::new();
assert_eq!(dc.issues, 0);
assert_eq!(dc.warnings, 0);
dc.pass("this is fine");
assert_eq!(dc.issues, 0);
assert_eq!(dc.warnings, 0);
dc.fail("something broke");
assert_eq!(dc.issues, 1);
assert_eq!(dc.warnings, 0);
dc.warn("be careful");
assert_eq!(dc.issues, 1);
assert_eq!(dc.warnings, 1);
dc.info("just info");
assert_eq!(dc.issues, 1);
assert_eq!(dc.warnings, 1);
dc.fail("another failure");
assert_eq!(dc.issues, 2);
assert_eq!(dc.warnings, 1);
}
#[test]
fn test_load_json_file_missing() {
let val = load_json_file(Path::new("/nonexistent/file.json"));
assert!(val.is_object());
assert!(val.as_object().unwrap().is_empty());
}
#[test]
fn test_load_json_file_valid() {
let dir = TempDir::new().unwrap();
let path = dir.path().join("test.json");
std::fs::write(&path, r#"{"key": "value"}"#).unwrap();
let val = load_json_file(&path);
assert_eq!(val["key"], "value");
}
#[test]
fn test_load_json_file_invalid_returns_empty() {
let dir = TempDir::new().unwrap();
let path = dir.path().join("bad.json");
std::fs::write(&path, "not valid json").unwrap();
let val = load_json_file(&path);
assert!(val.is_object());
assert!(val.as_object().unwrap().is_empty());
}
#[test]
fn test_load_json_file_strict_missing() {
let result = load_json_file_strict(Path::new("/nonexistent/file.json"));
assert!(result.is_ok());
let val = result.unwrap();
assert!(val.is_object());
assert!(val.as_object().unwrap().is_empty());
}
#[test]
fn test_load_json_file_strict_empty_file() {
let dir = TempDir::new().unwrap();
let path = dir.path().join("empty.json");
std::fs::write(&path, "").unwrap();
let result = load_json_file_strict(&path);
assert!(result.is_ok());
let val = result.unwrap();
assert!(val.as_object().unwrap().is_empty());
}
#[test]
fn test_load_json_file_strict_whitespace_only() {
let dir = TempDir::new().unwrap();
let path = dir.path().join("ws.json");
std::fs::write(&path, " \n \t ").unwrap();
let result = load_json_file_strict(&path);
assert!(result.is_ok());
}
#[test]
fn test_load_json_file_strict_invalid() {
let dir = TempDir::new().unwrap();
let path = dir.path().join("bad.json");
std::fs::write(&path, "not valid json").unwrap();
assert!(load_json_file_strict(&path).is_err());
}
#[test]
fn test_load_json_file_strict_valid() {
let dir = TempDir::new().unwrap();
let path = dir.path().join("good.json");
std::fs::write(&path, r#"{"hello": "world"}"#).unwrap();
let val = load_json_file_strict(&path).unwrap();
assert_eq!(val["hello"], "world");
}
#[test]
fn test_backup_config_file() {
let dir = TempDir::new().unwrap();
let path = dir.path().join("config.json");
std::fs::write(&path, r#"{"original": true}"#).unwrap();
let backup = backup_config_file(&path).unwrap();
assert!(backup.is_some());
let backup_path = backup.unwrap();
assert!(backup_path.exists());
let backup_content = std::fs::read_to_string(&backup_path).unwrap();
assert_eq!(backup_content, r#"{"original": true}"#);
}
#[test]
fn test_backup_config_file_missing() {
let result = backup_config_file(Path::new("/nonexistent/file.json")).unwrap();
assert!(result.is_none());
}
#[test]
fn test_safe_write_json_file() {
let dir = TempDir::new().unwrap();
let path = dir.path().join("output.json");
let value = serde_json::json!({"hello": "world"});
safe_write_json_file(&path, &value, None).unwrap();
let content: serde_json::Value =
serde_json::from_str(&std::fs::read_to_string(&path).unwrap()).unwrap();
assert_eq!(content["hello"], "world");
}
#[test]
fn test_safe_write_json_file_creates_parent_dirs() {
let dir = TempDir::new().unwrap();
let path = dir.path().join("deep/nested/dir/output.json");
let value = serde_json::json!({"nested": true});
safe_write_json_file(&path, &value, None).unwrap();
assert!(path.exists());
}
#[test]
fn test_safe_write_json_file_overwrites_existing() {
let dir = TempDir::new().unwrap();
let path = dir.path().join("existing.json");
std::fs::write(&path, r#"{"old": true}"#).unwrap();
let value = serde_json::json!({"new": true});
safe_write_json_file(&path, &value, None).unwrap();
let content: serde_json::Value =
serde_json::from_str(&std::fs::read_to_string(&path).unwrap()).unwrap();
assert_eq!(content["new"], true);
assert!(content.get("old").is_none());
}
#[test]
fn test_write_json_file() {
let dir = TempDir::new().unwrap();
let path = dir.path().join("write_test.json");
let value = serde_json::json!({"test": 42});
write_json_file(&path, &value).unwrap();
let content: serde_json::Value =
serde_json::from_str(&std::fs::read_to_string(&path).unwrap()).unwrap();
assert_eq!(content["test"], 42);
}
#[test]
fn test_load_toml_file_missing() {
let val = load_toml_file(Path::new("/nonexistent/file.toml"));
assert!(val.is_table());
assert!(val.as_table().unwrap().is_empty());
}
#[test]
fn test_load_toml_file_valid() {
let dir = TempDir::new().unwrap();
let path = dir.path().join("test.toml");
std::fs::write(&path, "key = \"value\"\nnumber = 42\n").unwrap();
let val = load_toml_file(&path);
assert!(val.is_table(), "load_toml_file should return a table");
assert!(
val.as_table().unwrap().is_empty(),
"load_toml_file returns empty table due to toml::Value parse limitation"
);
}
#[test]
fn test_load_toml_file_invalid_returns_empty() {
let dir = TempDir::new().unwrap();
let path = dir.path().join("bad.toml");
std::fs::write(&path, "{{{{not valid toml").unwrap();
let val = load_toml_file(&path);
assert!(val.is_table());
assert!(val.as_table().unwrap().is_empty());
}
#[test]
fn test_write_toml_file() {
let dir = TempDir::new().unwrap();
let path = dir.path().join("output.toml");
let mut table = toml::map::Map::new();
table.insert("key".to_string(), toml::Value::String("value".to_string()));
let val = toml::Value::Table(table);
write_toml_file(&path, &val).unwrap();
assert!(path.exists());
let content = std::fs::read_to_string(&path).unwrap();
assert!(content.contains("key"));
assert!(content.contains("value"));
}
#[test]
fn test_load_jsonc_file_missing() {
let val = load_jsonc_file(Path::new("/nonexistent/file.jsonc"));
assert!(val.is_object());
assert!(val.as_object().unwrap().is_empty());
}
#[test]
fn test_load_jsonc_file_with_comments() {
let dir = TempDir::new().unwrap();
let path = dir.path().join("test.jsonc");
std::fs::write(
&path,
r#"{
// This is a comment
"key": "value", // trailing comment
/* block comment */
"number": 42,
}"#,
)
.unwrap();
let val = load_jsonc_file(&path);
assert_eq!(val["key"], "value");
assert_eq!(val["number"], 42);
}
#[test]
fn test_load_jsonc_file_strict_missing() {
let result = load_jsonc_file_strict(Path::new("/nonexistent/file.jsonc"));
assert!(result.is_ok());
}
#[test]
fn test_load_jsonc_file_strict_with_comments() {
let dir = TempDir::new().unwrap();
let path = dir.path().join("test.jsonc");
std::fs::write(
&path,
r#"{
// comment
"key": "value"
}"#,
)
.unwrap();
let val = load_jsonc_file_strict(&path).unwrap();
assert_eq!(val["key"], "value");
}
#[test]
fn test_parse_jsonc() {
let input = r#"{
// line comment
"a": 1,
/* block */ "b": 2,
}"#;
let val = parse_jsonc(input);
assert_eq!(val["a"], 1);
assert_eq!(val["b"], 2);
}
#[test]
fn test_is_detected_claude() {
let dir = TempDir::new().unwrap();
let home = dir.path();
assert!(!ClaudeIntegration.is_detected(home));
std::fs::create_dir_all(home.join(".claude")).unwrap();
assert!(ClaudeIntegration.is_detected(home));
}
#[test]
fn test_is_detected_codex() {
let dir = TempDir::new().unwrap();
let home = dir.path();
assert!(!CodexIntegration.is_detected(home));
std::fs::create_dir_all(home.join(".codex")).unwrap();
assert!(CodexIntegration.is_detected(home));
}
#[test]
fn test_is_detected_gemini() {
let dir = TempDir::new().unwrap();
let home = dir.path();
assert!(!GeminiIntegration.is_detected(home));
std::fs::create_dir_all(home.join(".gemini")).unwrap();
assert!(GeminiIntegration.is_detected(home));
}
#[test]
fn test_is_detected_cursor() {
let dir = TempDir::new().unwrap();
let home = dir.path();
assert!(!CursorIntegration.is_detected(home));
std::fs::create_dir_all(home.join(".cursor")).unwrap();
assert!(CursorIntegration.is_detected(home));
}
#[test]
fn test_is_detected_opencode() {
let dir = TempDir::new().unwrap();
let home = dir.path();
assert!(!OpenCodeIntegration.is_detected(home));
std::fs::create_dir_all(home.join(".config/opencode")).unwrap();
assert!(OpenCodeIntegration.is_detected(home));
}
#[test]
fn test_is_detected_zed() {
let dir = TempDir::new().unwrap();
let home = dir.path();
assert!(!ZedIntegration.is_detected(home));
#[cfg(target_os = "macos")]
std::fs::create_dir_all(home.join("Library/Application Support/Zed")).unwrap();
#[cfg(not(target_os = "macos"))]
std::fs::create_dir_all(home.join(".config/zed")).unwrap();
assert!(ZedIntegration.is_detected(home));
}
#[test]
fn test_is_detected_copilot() {
let dir = TempDir::new().unwrap();
let home = dir.path();
assert!(!CopilotIntegration.is_detected(home));
std::fs::create_dir_all(home.join(".copilot")).unwrap();
assert!(CopilotIntegration.is_detected(home));
}
#[test]
fn test_has_tokensave_claude() {
let dir = TempDir::new().unwrap();
let home = dir.path();
assert!(!ClaudeIntegration.has_tokensave(home));
let ctx = make_install_ctx(home);
ClaudeIntegration.install(&ctx).unwrap();
assert!(ClaudeIntegration.has_tokensave(home));
ClaudeIntegration.uninstall(&ctx).unwrap();
assert!(!ClaudeIntegration.has_tokensave(home));
}
#[test]
fn test_has_tokensave_gemini() {
let dir = TempDir::new().unwrap();
let home = dir.path();
assert!(!GeminiIntegration.has_tokensave(home));
let ctx = make_install_ctx(home);
GeminiIntegration.install(&ctx).unwrap();
assert!(GeminiIntegration.has_tokensave(home));
}
#[test]
fn test_has_tokensave_codex() {
let dir = TempDir::new().unwrap();
let home = dir.path();
assert!(!CodexIntegration.has_tokensave(home));
let ctx = make_install_ctx(home);
CodexIntegration.install(&ctx).unwrap();
assert!(home.join(".codex/config.toml").exists());
assert!(
!CodexIntegration.has_tokensave(home),
"has_tokensave returns false due to toml::Value parse limitation"
);
}
#[test]
fn test_has_tokensave_cursor() {
let dir = TempDir::new().unwrap();
let home = dir.path();
assert!(!CursorIntegration.has_tokensave(home));
let ctx = make_install_ctx(home);
CursorIntegration.install(&ctx).unwrap();
assert!(CursorIntegration.has_tokensave(home));
}
#[test]
fn test_has_tokensave_opencode() {
let dir = TempDir::new().unwrap();
let home = dir.path();
assert!(!OpenCodeIntegration.has_tokensave(home));
let ctx = make_install_ctx(home);
OpenCodeIntegration.install(&ctx).unwrap();
assert!(OpenCodeIntegration.has_tokensave(home));
}
#[test]
fn test_has_tokensave_copilot() {
let dir = TempDir::new().unwrap();
let home = dir.path();
assert!(!CopilotIntegration.has_tokensave(home));
let ctx = make_install_ctx(home);
CopilotIntegration.install(&ctx).unwrap();
assert!(CopilotIntegration.has_tokensave(home));
}
#[test]
fn test_claude_install_idempotent() {
let dir = TempDir::new().unwrap();
let home = dir.path();
let ctx = make_install_ctx(home);
ClaudeIntegration.install(&ctx).unwrap();
ClaudeIntegration.install(&ctx).unwrap();
let claude_json: serde_json::Value = serde_json::from_str(
&std::fs::read_to_string(home.join(".claude.json")).unwrap(),
)
.unwrap();
assert!(claude_json["mcpServers"]["tokensave"].is_object());
}
#[test]
fn test_gemini_install_idempotent() {
let dir = TempDir::new().unwrap();
let home = dir.path();
let ctx = make_install_ctx(home);
GeminiIntegration.install(&ctx).unwrap();
GeminiIntegration.install(&ctx).unwrap();
let settings: serde_json::Value = serde_json::from_str(
&std::fs::read_to_string(home.join(".gemini/settings.json")).unwrap(),
)
.unwrap();
assert!(settings["mcpServers"]["tokensave"].is_object());
}
#[test]
fn test_uninstall_without_install_does_not_crash() {
let dir = TempDir::new().unwrap();
let home = dir.path();
let ctx = make_install_ctx(home);
ClaudeIntegration.uninstall(&ctx).unwrap();
GeminiIntegration.uninstall(&ctx).unwrap();
CodexIntegration.uninstall(&ctx).unwrap();
CursorIntegration.uninstall(&ctx).unwrap();
CopilotIntegration.uninstall(&ctx).unwrap();
OpenCodeIntegration.uninstall(&ctx).unwrap();
ZedIntegration.uninstall(&ctx).unwrap();
ClineIntegration.uninstall(&ctx).unwrap();
RooCodeIntegration.uninstall(&ctx).unwrap();
}
#[test]
fn test_claude_install_preserves_existing_claude_json() {
let dir = TempDir::new().unwrap();
let home = dir.path();
let claude_json_path = home.join(".claude.json");
std::fs::write(
&claude_json_path,
r#"{"mcpServers": {"other-server": {"command": "foo"}}, "customKey": 42}"#,
)
.unwrap();
let ctx = make_install_ctx(home);
ClaudeIntegration.install(&ctx).unwrap();
let content: serde_json::Value =
serde_json::from_str(&std::fs::read_to_string(&claude_json_path).unwrap()).unwrap();
assert!(content["mcpServers"]["tokensave"].is_object());
assert!(content["mcpServers"]["other-server"].is_object());
assert_eq!(content["customKey"], 42);
}
#[test]
fn test_gemini_install_preserves_existing_settings() {
let dir = TempDir::new().unwrap();
let home = dir.path();
let settings_path = home.join(".gemini/settings.json");
std::fs::create_dir_all(home.join(".gemini")).unwrap();
std::fs::write(
&settings_path,
r#"{"mcpServers": {"other": {"command": "bar"}}, "theme": "dark"}"#,
)
.unwrap();
let ctx = make_install_ctx(home);
GeminiIntegration.install(&ctx).unwrap();
let content: serde_json::Value =
serde_json::from_str(&std::fs::read_to_string(&settings_path).unwrap()).unwrap();
assert!(content["mcpServers"]["tokensave"].is_object());
assert!(content["mcpServers"]["other"].is_object());
assert_eq!(content["theme"], "dark");
}
#[test]
fn test_tool_names_not_empty() {
assert!(!TOOL_NAMES.is_empty());
for name in TOOL_NAMES {
assert!(name.starts_with("tokensave_"), "tool name should start with tokensave_: {name}");
}
}
#[test]
fn test_expected_tool_perms_not_empty() {
assert!(!EXPECTED_TOOL_PERMS.is_empty());
for perm in EXPECTED_TOOL_PERMS {
assert!(
perm.starts_with("mcp__tokensave__"),
"tool perm should start with mcp__tokensave__: {perm}"
);
}
}
#[test]
fn test_tool_perms_match_tool_names() {
assert_eq!(
TOOL_NAMES.len(),
EXPECTED_TOOL_PERMS.len(),
"TOOL_NAMES and EXPECTED_TOOL_PERMS should have same length"
);
for name in TOOL_NAMES {
let expected_perm = format!("mcp__tokensave__{name}");
assert!(
EXPECTED_TOOL_PERMS.contains(&expected_perm.as_str()),
"missing permission for tool {name}: expected {expected_perm}"
);
}
}