readd tests, remove markdown files

This commit is contained in:
Alex Gorevski 2026-02-17 16:08:53 -08:00 committed by Chummy
parent e2634c72c2
commit 9a6fa76825
17 changed files with 1352 additions and 0 deletions

View file

@ -493,4 +493,85 @@ mod tests {
.unwrap_or("")
.contains("Rate limit exceeded"));
}
#[tokio::test]
async fn delegate_context_is_prepended_to_prompt() {
let mut agents = HashMap::new();
agents.insert(
"tester".to_string(),
DelegateAgentConfig {
provider: "invalid-for-test".to_string(),
model: "test-model".to_string(),
system_prompt: None,
api_key: None,
temperature: None,
max_depth: 3,
},
);
let tool = DelegateTool::new(agents, None, test_security());
let result = tool
.execute(json!({
"agent": "tester",
"prompt": "do something",
"context": "some context data"
}))
.await
.unwrap();
assert!(!result.success);
assert!(result
.error
.as_deref()
.unwrap_or("")
.contains("Failed to create provider"));
}
#[tokio::test]
async fn delegate_empty_context_omits_prefix() {
let mut agents = HashMap::new();
agents.insert(
"tester".to_string(),
DelegateAgentConfig {
provider: "invalid-for-test".to_string(),
model: "test-model".to_string(),
system_prompt: None,
api_key: None,
temperature: None,
max_depth: 3,
},
);
let tool = DelegateTool::new(agents, None, test_security());
let result = tool
.execute(json!({
"agent": "tester",
"prompt": "do something",
"context": ""
}))
.await
.unwrap();
assert!(!result.success);
assert!(result
.error
.as_deref()
.unwrap_or("")
.contains("Failed to create provider"));
}
#[test]
fn delegate_depth_construction() {
let tool = DelegateTool::with_depth(sample_agents(), None, test_security(), 5);
assert_eq!(tool.depth, 5);
}
#[tokio::test]
async fn delegate_no_agents_configured() {
let tool = DelegateTool::new(HashMap::new(), None, test_security());
let result = tool
.execute(json!({"agent": "any", "prompt": "test"}))
.await
.unwrap();
assert!(!result.success);
assert!(result.error.unwrap().contains("none configured"));
}
}

View file

@ -407,4 +407,62 @@ mod tests {
let _ = tokio::fs::remove_dir_all(&dir).await;
}
// ── §5.1 TOCTOU / symlink file write protection tests ────
#[cfg(unix)]
#[tokio::test]
async fn file_write_blocks_symlink_target_file() {
use std::os::unix::fs::symlink;
let root = std::env::temp_dir().join("zeroclaw_test_file_write_symlink_target");
let workspace = root.join("workspace");
let outside = root.join("outside");
let _ = tokio::fs::remove_dir_all(&root).await;
tokio::fs::create_dir_all(&workspace).await.unwrap();
tokio::fs::create_dir_all(&outside).await.unwrap();
// Create a file outside and symlink to it inside workspace
tokio::fs::write(outside.join("target.txt"), "original")
.await
.unwrap();
symlink(outside.join("target.txt"), workspace.join("linked.txt")).unwrap();
let tool = FileWriteTool::new(test_security(workspace.clone()));
let result = tool
.execute(json!({"path": "linked.txt", "content": "overwritten"}))
.await
.unwrap();
assert!(!result.success, "writing through symlink must be blocked");
assert!(
result.error.as_deref().unwrap_or("").contains("symlink"),
"error should mention symlink"
);
// Verify original file was not modified
let content = tokio::fs::read_to_string(outside.join("target.txt"))
.await
.unwrap();
assert_eq!(content, "original", "original file must not be modified");
let _ = tokio::fs::remove_dir_all(&root).await;
}
#[tokio::test]
async fn file_write_blocks_null_byte_in_path() {
let dir = std::env::temp_dir().join("zeroclaw_test_file_write_null");
let _ = tokio::fs::remove_dir_all(&dir).await;
tokio::fs::create_dir_all(&dir).await.unwrap();
let tool = FileWriteTool::new(test_security(dir.clone()));
let result = tool
.execute(json!({"path": "file\u{0000}.txt", "content": "bad"}))
.await
.unwrap();
assert!(!result.success, "paths with null bytes must be blocked");
let _ = tokio::fs::remove_dir_all(&dir).await;
}
}

View file

@ -808,4 +808,73 @@ mod tests {
let tool = test_tool(vec!["example.com"]);
assert_eq!(tool.name(), "http_request");
}
// ── §1.4 DNS rebinding / SSRF defense-in-depth tests ─────
#[test]
fn ssrf_blocks_loopback_127_range() {
assert!(is_private_or_local_host("127.0.0.1"));
assert!(is_private_or_local_host("127.0.0.2"));
assert!(is_private_or_local_host("127.255.255.255"));
}
#[test]
fn ssrf_blocks_rfc1918_10_range() {
assert!(is_private_or_local_host("10.0.0.1"));
assert!(is_private_or_local_host("10.255.255.255"));
}
#[test]
fn ssrf_blocks_rfc1918_172_range() {
assert!(is_private_or_local_host("172.16.0.1"));
assert!(is_private_or_local_host("172.31.255.255"));
}
#[test]
fn ssrf_blocks_unspecified_address() {
assert!(is_private_or_local_host("0.0.0.0"));
}
#[test]
fn ssrf_blocks_dot_localhost_subdomain() {
assert!(is_private_or_local_host("evil.localhost"));
assert!(is_private_or_local_host("a.b.localhost"));
}
#[test]
fn ssrf_blocks_dot_local_tld() {
assert!(is_private_or_local_host("service.local"));
}
#[test]
fn ssrf_ipv6_unspecified() {
assert!(is_private_or_local_host("::"));
}
#[test]
fn validate_rejects_ftp_scheme() {
let tool = test_tool(vec!["example.com"]);
let err = tool
.validate_url("ftp://example.com")
.unwrap_err()
.to_string();
assert!(err.contains("http://") || err.contains("https://"));
}
#[test]
fn validate_rejects_empty_url() {
let tool = test_tool(vec!["example.com"]);
let err = tool.validate_url("").unwrap_err().to_string();
assert!(err.contains("empty"));
}
#[test]
fn validate_rejects_ipv6_host() {
let tool = test_tool(vec!["example.com"]);
let err = tool
.validate_url("http://[::1]:8080/path")
.unwrap_err()
.to_string();
assert!(err.contains("IPv6"));
}
}

View file

@ -365,4 +365,62 @@ mod tests {
let _ = std::fs::remove_file(std::env::temp_dir().join("zeroclaw_shell_approval_test"));
}
// ── §5.2 Shell timeout enforcement tests ─────────────────
#[test]
fn shell_timeout_constant_is_reasonable() {
assert_eq!(SHELL_TIMEOUT_SECS, 60, "shell timeout must be 60 seconds");
}
#[test]
fn shell_output_limit_is_1mb() {
assert_eq!(
MAX_OUTPUT_BYTES, 1_048_576,
"max output must be 1 MB to prevent OOM"
);
}
// ── §5.3 Non-UTF8 binary output tests ────────────────────
#[test]
fn shell_safe_env_vars_excludes_secrets() {
for var in SAFE_ENV_VARS {
let lower = var.to_lowercase();
assert!(
!lower.contains("key") && !lower.contains("secret") && !lower.contains("token"),
"SAFE_ENV_VARS must not include sensitive variable: {var}"
);
}
}
#[test]
fn shell_safe_env_vars_includes_essentials() {
assert!(
SAFE_ENV_VARS.contains(&"PATH"),
"PATH must be in safe env vars"
);
assert!(
SAFE_ENV_VARS.contains(&"HOME"),
"HOME must be in safe env vars"
);
assert!(
SAFE_ENV_VARS.contains(&"TERM"),
"TERM must be in safe env vars"
);
}
#[tokio::test]
async fn shell_blocks_rate_limited() {
let security = Arc::new(SecurityPolicy {
autonomy: AutonomyLevel::Supervised,
max_actions_per_hour: 0,
workspace_dir: std::env::temp_dir(),
..SecurityPolicy::default()
});
let tool = ShellTool::new(security, test_runtime());
let result = tool.execute(json!({"command": "echo test"})).await.unwrap();
assert!(!result.success);
assert!(result.error.as_deref().unwrap_or("").contains("Rate limit"));
}
}