readd tests, remove markdown files
This commit is contained in:
parent
e2634c72c2
commit
9a6fa76825
17 changed files with 1352 additions and 0 deletions
|
|
@ -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"));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue