Merge pull request #2803 from zeroclaw-labs/issue-2746-capability-aware-tests-dev

test(infra): add capability-aware handling for sandbox-restricted test environments
This commit is contained in:
Argenis 2026-03-05 01:55:00 -05:00 committed by GitHub
commit bd2beb3e16
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 76 additions and 2 deletions

View File

@ -69,6 +69,7 @@ pub mod runtime;
pub(crate) mod security;
pub(crate) mod service;
pub(crate) mod skills;
pub mod test_capabilities;
pub mod tools;
pub(crate) mod tunnel;
pub mod update;

View File

@ -1287,7 +1287,15 @@ mod tests {
}),
);
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
if let Err(err) = std::net::TcpListener::bind("127.0.0.1:0") {
let reason = format!("loopback bind unavailable: {err}");
eprintln!("Skipping loopback-dependent Anthropic test: {reason}");
return;
}
let listener = TcpListener::bind("127.0.0.1:0")
.await
.expect("loopback bind should be available after capability check");
let addr = listener.local_addr().unwrap();
let server_handle = tokio::spawn(async move {
axum::serve(listener, app).await.unwrap();

56
src/test_capabilities.rs Normal file
View File

@ -0,0 +1,56 @@
//! Lightweight capability probes used by tests in constrained environments.
//!
//! These helpers let tests skip gracefully when sandbox restrictions prevent
//! operations like loopback binds or writable-home access.
use std::env;
use std::fs;
use std::net::TcpListener;
use std::path::{Path, PathBuf};
use std::time::{SystemTime, UNIX_EPOCH};
/// Return the configured home directory from environment variables.
pub fn home_dir_from_env() -> Option<PathBuf> {
env::var_os("HOME")
.or_else(|| env::var_os("USERPROFILE"))
.filter(|value| !value.is_empty())
.map(PathBuf::from)
}
/// Check that a directory is writable by creating and deleting a tiny probe file.
pub fn check_writable_dir(path: &Path) -> Result<(), String> {
fs::create_dir_all(path).map_err(|err| {
format!(
"failed to create directory {} for capability probe: {err}",
path.display()
)
})?;
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|duration| duration.as_nanos())
.unwrap_or(0);
let probe_name = format!(".zeroclaw-capability-probe-{}-{nanos}", std::process::id());
let probe_path = path.join(probe_name);
fs::write(&probe_path, b"probe")
.map_err(|err| format!("failed to write probe file {}: {err}", probe_path.display()))?;
if let Err(err) = fs::remove_file(&probe_path) {
return Err(format!(
"failed to clean up probe file {}: {err}",
probe_path.display()
));
}
Ok(())
}
/// Verify loopback bind capability for local mock servers used in tests.
pub fn check_loopback_bind() -> Result<(), String> {
TcpListener::bind("127.0.0.1:0")
.map(|listener| {
drop(listener);
})
.map_err(|err| format!("loopback bind unavailable: {err}"))
}

View File

@ -35,7 +35,16 @@ use std::path::PathBuf;
#[ignore = "requires live Gemini OAuth credentials with refresh_token"]
async fn gemini_warmup_refreshes_expired_oauth_token() -> Result<()> {
// Find ~/.zeroclaw/auth-profiles.json
let home = env::var("HOME").expect("HOME env var not set");
let Some(home) = zeroclaw::test_capabilities::home_dir_from_env() else {
eprintln!("⚠️ Skipping test: neither HOME nor USERPROFILE is set");
return Ok(());
};
if let Err(reason) = zeroclaw::test_capabilities::check_writable_dir(&home) {
eprintln!("⚠️ Skipping test: home directory is not writable ({reason})");
return Ok(());
}
let zeroclaw_dir = PathBuf::from(home).join(".zeroclaw");
let auth_profiles_path = zeroclaw_dir.join("auth-profiles.json");