Skip to content
28 changes: 28 additions & 0 deletions crates/chat-cli/src/cli/chat/tools/custom_tool.rs
Original file line number Diff line number Diff line change
Expand Up @@ -196,3 +196,31 @@ impl CustomTool {
}
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn default_timeout_is_two_minutes_in_ms() {
assert_eq!(default_timeout(), 120 * 1000);
}

#[test]
fn get_default_scopes_returns_non_empty() {
let scopes = get_default_scopes();
assert!(!scopes.is_empty(), "default OAuth scopes must not be empty");
}

#[test]
fn get_default_scopes_are_strings() {
for scope in get_default_scopes() {
assert!(!scope.is_empty(), "each scope must be a non-empty string");
}
}

#[test]
fn transport_type_default_is_stdio() {
assert!(matches!(TransportType::default(), TransportType::Stdio));
}
}
67 changes: 67 additions & 0 deletions crates/chat-cli/src/cli/chat/tools/delegate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -599,4 +599,71 @@ mod tests {
let schema = schemars::schema_for!(Delegate);
println!("{}", serde_json::to_string_pretty(&schema).unwrap());
}

// ── truncate_description ─────────────────────────────────────────────────

#[test]
fn truncate_at_first_period() {
assert_eq!(truncate_description("Short desc. More text."), "Short desc.");
}

#[test]
fn truncate_long_string_without_period() {
let long = "a".repeat(80);
let result = truncate_description(&long);
assert_eq!(result.len(), 57);
}

#[test]
fn truncate_short_string_without_period_unchanged() {
assert_eq!(truncate_description("short"), "short");
}

// ── format_launch_success ────────────────────────────────────────────────

#[test]
fn format_launch_success_contains_agent_and_task() {
let msg = format_launch_success("my-agent", "do the thing");
assert!(msg.contains("my-agent"));
assert!(msg.contains("do the thing"));
}

// ── AgentStatus ──────────────────────────────────────────────────────────

#[test]
fn agent_status_default_is_running() {
assert!(matches!(AgentStatus::default(), AgentStatus::Running));
}

// ── Delegate deserialization ─────────────────────────────────────────────

#[test]
fn deserialize_launch() {
let v = serde_json::json!({
"operation": "launch",
"task": "write a hello world program"
});
let d = serde_json::from_value::<Delegate>(v).unwrap();
assert!(matches!(d.operation, Operation::Launch));
assert_eq!(d.task.as_deref(), Some("write a hello world program"));
}

#[test]
fn deserialize_status_all() {
let v = serde_json::json!({ "operation": "status" });
let d = serde_json::from_value::<Delegate>(v).unwrap();
assert!(matches!(d.operation, Operation::Status));
assert!(d.agent.is_none());
}

#[test]
fn resolve_agent_name_fallback_chain() {
// explicit > configured_default > DEFAULT_AGENT_NAME
fn resolve<'a>(explicit: Option<&'a str>, configured: Option<&'a str>) -> &'a str {
explicit.or(configured).unwrap_or(DEFAULT_AGENT_NAME)
}
assert_eq!(resolve(Some("explicit"), Some("configured")), "explicit");
assert_eq!(resolve(None, Some("configured")), "configured");
assert_eq!(resolve(None, None), DEFAULT_AGENT_NAME);
}
}
26 changes: 26 additions & 0 deletions crates/chat-cli/src/cli/chat/tools/introspect.rs
Original file line number Diff line number Diff line change
Expand Up @@ -186,3 +186,29 @@ impl Introspect {
Ok(())
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn deserialize_with_query() {
let v = serde_json::json!({ "query": "how do I use /compact?" });
let i = serde_json::from_value::<Introspect>(v).unwrap();
assert_eq!(i.query, Some("how do I use /compact?".to_string()));
}

#[test]
fn deserialize_without_query() {
let v = serde_json::json!({});
let i = serde_json::from_value::<Introspect>(v).unwrap();
assert_eq!(i.query, None);
}

#[tokio::test]
async fn validate_always_succeeds() {
let i = Introspect { query: None };
let os = crate::os::Os::new().await.unwrap();
assert!(i.validate(&os).await.is_ok());
}
}
43 changes: 43 additions & 0 deletions crates/chat-cli/src/cli/chat/tools/knowledge.rs
Original file line number Diff line number Diff line change
Expand Up @@ -579,3 +579,46 @@ impl Knowledge {
output.trim_end().to_string() // Remove trailing newline
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn deserialize_add() {
let v = serde_json::json!({
"command": "add",
"name": "my-kb",
"value": "/some/path"
});
let k = serde_json::from_value::<Knowledge>(v).unwrap();
assert!(matches!(k, Knowledge::Add(_)));
}

#[test]
fn deserialize_search() {
let v = serde_json::json!({
"command": "search",
"query": "find something"
});
let k = serde_json::from_value::<Knowledge>(v).unwrap();
assert!(matches!(k, Knowledge::Search(_)));
}

#[test]
fn deserialize_remove() {
let v = serde_json::json!({
"command": "remove",
"identifier": "my-kb"
});
let k = serde_json::from_value::<Knowledge>(v).unwrap();
assert!(matches!(k, Knowledge::Remove(_)));
}

#[test]
fn deserialize_clear() {
let v = serde_json::json!({ "command": "clear", "confirm": true });
let k = serde_json::from_value::<Knowledge>(v).unwrap();
assert!(matches!(k, Knowledge::Clear(_)));
}
}
22 changes: 22 additions & 0 deletions crates/chat-cli/src/cli/chat/tools/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -615,4 +615,26 @@ mod tests {
)
.await;
}

#[tokio::test]
async fn test_sanitize_path_absolute_unchanged() {
let os = Os::new().await.unwrap();
let actual = sanitize_path_tool_arg(&os, "/absolute/path");
assert_eq!(actual, os.fs.chroot_path("/absolute/path"));
}

#[tokio::test]
async fn test_sanitize_path_empty_string() {
let os = Os::new().await.unwrap();
// Empty string should not panic — result is implementation-defined but stable
let _ = sanitize_path_tool_arg(&os, "");
}

#[tokio::test]
async fn test_sanitize_path_tilde_only_expands_at_start() {
let os = Os::new().await.unwrap();
// Tilde in the middle should not expand
let actual = sanitize_path_tool_arg(&os, "/foo/~/bar");
assert_eq!(actual, os.fs.chroot_path("/foo/~/bar"));
}
}
33 changes: 33 additions & 0 deletions crates/chat-cli/src/cli/chat/tools/thinking.rs
Original file line number Diff line number Diff line change
Expand Up @@ -71,3 +71,36 @@ impl Thinking {
Ok(())
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn deserialize_thinking() {
let v = serde_json::json!({ "thought": "let me reason through this" });
let t = serde_json::from_value::<Thinking>(v).unwrap();
assert_eq!(t.thought, "let me reason through this");
}

#[tokio::test]
async fn invoke_returns_empty_output() {
let t = Thinking { thought: "some thought".to_string() };
let result = t.invoke(std::io::sink()).await.unwrap();
assert!(matches!(result.output, OutputKind::Text(ref s) if s.is_empty()));
}

#[tokio::test]
async fn validate_accepts_empty_thought() {
let mut t = Thinking { thought: String::new() };
let os = crate::os::Os::new().await.unwrap();
assert!(t.validate(&os).await.is_ok());
}

#[tokio::test]
async fn validate_accepts_non_empty_thought() {
let mut t = Thinking { thought: "complex reasoning".to_string() };
let os = crate::os::Os::new().await.unwrap();
assert!(t.validate(&os).await.is_ok());
}
}
88 changes: 88 additions & 0 deletions crates/chat-cli/src/cli/chat/tools/todo.rs
Original file line number Diff line number Diff line change
Expand Up @@ -481,3 +481,91 @@ where
let mut seen = HashSet::with_capacity(vec.len());
vec.iter().any(|item| !seen.insert(item))
}

#[cfg(test)]
mod tests {
use super::*;

// ── has_duplicates ───────────────────────────────────────────────────────

#[test]
fn has_duplicates_empty() {
assert!(!has_duplicates::<usize>(&[]));
}

#[test]
fn has_duplicates_unique() {
assert!(!has_duplicates(&[1, 2, 3]));
}

#[test]
fn has_duplicates_with_duplicate() {
assert!(has_duplicates(&[1, 2, 1]));
}

// ── generate_new_todo_id ─────────────────────────────────────────────────

#[test]
fn generate_new_todo_id_is_unique() {
// IDs are millisecond timestamps — sleep to guarantee different values
let a = generate_new_todo_id();
std::thread::sleep(std::time::Duration::from_millis(2));
let b = generate_new_todo_id();
assert_ne!(a, b, "IDs generated at different times must be unique");
}

#[test]
fn generate_new_todo_id_format() {
let id = generate_new_todo_id();
assert!(!id.is_empty());
assert!(id.chars().all(|c| c.is_ascii_digit()), "ID must be numeric: {id}");
}

// ── TodoList deserialization ─────────────────────────────────────────────

#[test]
fn deserialize_create() {
let v = serde_json::json!({
"command": "create",
"todo_list_description": "test todo",
"tasks": ["task 1", "task 2"]
});
let tl = serde_json::from_value::<TodoList>(v).unwrap();
assert!(matches!(tl, TodoList::Create { .. }));
}

#[test]
fn deserialize_complete() {
let v = serde_json::json!({
"command": "complete",
"current_id": "abc123",
"completed_indices": [0],
"context_update": "done"
});
let tl = serde_json::from_value::<TodoList>(v).unwrap();
assert!(matches!(tl, TodoList::Complete { .. }));
}

#[test]
fn deserialize_add() {
let v = serde_json::json!({
"command": "add",
"current_id": "abc123",
"new_tasks": ["new task"],
"insert_indices": [0]
});
let tl = serde_json::from_value::<TodoList>(v).unwrap();
assert!(matches!(tl, TodoList::Add { .. }));
}

#[test]
fn deserialize_remove() {
let v = serde_json::json!({
"command": "remove",
"current_id": "abc123",
"remove_indices": [1]
});
let tl = serde_json::from_value::<TodoList>(v).unwrap();
assert!(matches!(tl, TodoList::Remove { .. }));
}
}
43 changes: 43 additions & 0 deletions crates/chat-cli/src/util/env_var.rs
Original file line number Diff line number Diff line change
Expand Up @@ -94,3 +94,46 @@ pub fn get_all_env_vars() -> std::env::Vars {
pub fn get_telemetry_client_id(env: &Env) -> Result<String, std::env::VarError> {
env.get(Q_TELEMETRY_CLIENT_ID)
}

#[cfg(test)]
mod tests {
use super::*;
use crate::os::Env;

#[test]
fn get_mock_chat_response_present() {
let env = Env::from_slice(&[("Q_MOCK_CHAT_RESPONSE", "hello")]);
assert_eq!(get_mock_chat_response(&env), Some("hello".to_string()));
}

#[test]
fn get_mock_chat_response_absent() {
let env = Env::from_slice(&[]);
assert_eq!(get_mock_chat_response(&env), None);
}

#[test]
fn is_sigv4_enabled_with_value() {
let env = Env::from_slice(&[("AMAZON_Q_SIGV4", "true")]);
assert!(is_sigv4_enabled(&env));
}

#[test]
fn is_sigv4_enabled_empty_value() {
let env = Env::from_slice(&[("AMAZON_Q_SIGV4", "")]);
assert!(!is_sigv4_enabled(&env));
}

#[test]
fn is_sigv4_enabled_absent() {
let env = Env::from_slice(&[]);
assert!(!is_sigv4_enabled(&env));
}

#[test]
fn get_editor_fallback() {
// get_editor() reads from real env — just verify it returns a non-empty string
let editor = get_editor();
assert!(!editor.is_empty());
}
}
Loading