init: push source
This commit is contained in:
179
src-tauri/tests/sandbox/common/claude_real.rs
Normal file
179
src-tauri/tests/sandbox/common/claude_real.rs
Normal file
@@ -0,0 +1,179 @@
|
||||
//! Helper functions for executing real Claude commands in tests
|
||||
use anyhow::{Context, Result};
|
||||
use std::path::Path;
|
||||
use std::process::{Command, Stdio};
|
||||
use std::time::Duration;
|
||||
|
||||
/// Execute Claude with a specific task and capture output
|
||||
pub fn execute_claude_task(
|
||||
project_path: &Path,
|
||||
task: &str,
|
||||
system_prompt: Option<&str>,
|
||||
model: Option<&str>,
|
||||
sandbox_profile_id: Option<i64>,
|
||||
timeout_secs: u64,
|
||||
) -> Result<ClaudeOutput> {
|
||||
let mut cmd = Command::new("claude");
|
||||
|
||||
// Add task
|
||||
cmd.arg("-p").arg(task);
|
||||
|
||||
// Add system prompt if provided
|
||||
if let Some(prompt) = system_prompt {
|
||||
cmd.arg("--system-prompt").arg(prompt);
|
||||
}
|
||||
|
||||
// Add model if provided
|
||||
if let Some(m) = model {
|
||||
cmd.arg("--model").arg(m);
|
||||
}
|
||||
|
||||
// Always add these flags for testing
|
||||
cmd.arg("--output-format").arg("stream-json")
|
||||
.arg("--verbose")
|
||||
.arg("--dangerously-skip-permissions")
|
||||
.current_dir(project_path)
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped());
|
||||
|
||||
// Add sandbox profile ID if provided
|
||||
if let Some(profile_id) = sandbox_profile_id {
|
||||
cmd.env("CLAUDIA_SANDBOX_PROFILE_ID", profile_id.to_string());
|
||||
}
|
||||
|
||||
// Execute with timeout (use gtimeout on macOS, timeout on Linux)
|
||||
let start = std::time::Instant::now();
|
||||
|
||||
let timeout_cmd = if cfg!(target_os = "macos") {
|
||||
// On macOS, try gtimeout (from GNU coreutils) first, fallback to direct execution
|
||||
if std::process::Command::new("which")
|
||||
.arg("gtimeout")
|
||||
.output()
|
||||
.map(|o| o.status.success())
|
||||
.unwrap_or(false)
|
||||
{
|
||||
"gtimeout"
|
||||
} else {
|
||||
// If gtimeout not available, just run without timeout
|
||||
""
|
||||
}
|
||||
} else {
|
||||
"timeout"
|
||||
};
|
||||
|
||||
let output = if timeout_cmd.is_empty() {
|
||||
// Run without timeout wrapper
|
||||
cmd.output()
|
||||
.context("Failed to execute Claude command")?
|
||||
} else {
|
||||
// Run with timeout wrapper
|
||||
let mut timeout_cmd = Command::new(timeout_cmd);
|
||||
timeout_cmd.arg(timeout_secs.to_string())
|
||||
.arg("claude")
|
||||
.args(cmd.get_args())
|
||||
.current_dir(project_path)
|
||||
.envs(cmd.get_envs().filter_map(|(k, v)| v.map(|v| (k, v))))
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.output()
|
||||
.context("Failed to execute Claude command with timeout")?
|
||||
};
|
||||
|
||||
let duration = start.elapsed();
|
||||
|
||||
Ok(ClaudeOutput {
|
||||
stdout: String::from_utf8_lossy(&output.stdout).to_string(),
|
||||
stderr: String::from_utf8_lossy(&output.stderr).to_string(),
|
||||
exit_code: output.status.code().unwrap_or(-1),
|
||||
duration,
|
||||
})
|
||||
}
|
||||
|
||||
/// Result of Claude execution
|
||||
#[derive(Debug)]
|
||||
pub struct ClaudeOutput {
|
||||
pub stdout: String,
|
||||
pub stderr: String,
|
||||
pub exit_code: i32,
|
||||
pub duration: Duration,
|
||||
}
|
||||
|
||||
impl ClaudeOutput {
|
||||
/// Check if the output contains evidence of a specific operation
|
||||
pub fn contains_operation(&self, operation: &str) -> bool {
|
||||
self.stdout.contains(operation) || self.stderr.contains(operation)
|
||||
}
|
||||
|
||||
/// Check if operation was blocked (look for permission denied, sandbox violation, etc)
|
||||
pub fn operation_was_blocked(&self, operation: &str) -> bool {
|
||||
let blocked_patterns = [
|
||||
"permission denied",
|
||||
"not permitted",
|
||||
"blocked by sandbox",
|
||||
"operation not allowed",
|
||||
"access denied",
|
||||
"sandbox violation",
|
||||
];
|
||||
|
||||
let output = format!("{}\n{}", self.stdout, self.stderr).to_lowercase();
|
||||
let op_lower = operation.to_lowercase();
|
||||
|
||||
// Check if operation was mentioned along with a block pattern
|
||||
blocked_patterns.iter().any(|pattern| {
|
||||
output.contains(&op_lower) && output.contains(pattern)
|
||||
})
|
||||
}
|
||||
|
||||
/// Check if file read was successful
|
||||
pub fn file_read_succeeded(&self, filename: &str) -> bool {
|
||||
// Look for patterns indicating successful file read
|
||||
let patterns = [
|
||||
&format!("Read {}", filename),
|
||||
&format!("Reading {}", filename),
|
||||
&format!("Contents of {}", filename),
|
||||
"test content", // Our test files contain this
|
||||
];
|
||||
|
||||
patterns.iter().any(|pattern| self.contains_operation(pattern))
|
||||
}
|
||||
|
||||
/// Check if network connection was attempted
|
||||
pub fn network_attempted(&self, host: &str) -> bool {
|
||||
let patterns = [
|
||||
&format!("Connecting to {}", host),
|
||||
&format!("Connected to {}", host),
|
||||
&format!("connect to {}", host),
|
||||
host,
|
||||
];
|
||||
|
||||
patterns.iter().any(|pattern| self.contains_operation(pattern))
|
||||
}
|
||||
}
|
||||
|
||||
/// Common test tasks for Claude
|
||||
pub mod tasks {
|
||||
/// Task to read a file
|
||||
pub fn read_file(filename: &str) -> String {
|
||||
format!("Read the file {} and show me its contents", filename)
|
||||
}
|
||||
|
||||
/// Task to attempt network connection
|
||||
pub fn connect_network(host: &str) -> String {
|
||||
format!("Try to connect to {} and tell me if it works", host)
|
||||
}
|
||||
|
||||
/// Task to do multiple operations
|
||||
pub fn multi_operation() -> String {
|
||||
"Read the file ./test.txt in the current directory and show its contents".to_string()
|
||||
}
|
||||
|
||||
/// Task to test file write
|
||||
pub fn write_file(filename: &str, content: &str) -> String {
|
||||
format!("Create a file called {} with the content '{}'", filename, content)
|
||||
}
|
||||
|
||||
/// Task to test process spawning
|
||||
pub fn spawn_process(command: &str) -> String {
|
||||
format!("Run the command '{}' and show me the output", command)
|
||||
}
|
||||
}
|
333
src-tauri/tests/sandbox/common/fixtures.rs
Normal file
333
src-tauri/tests/sandbox/common/fixtures.rs
Normal file
@@ -0,0 +1,333 @@
|
||||
//! Test fixtures and data for sandbox testing
|
||||
use anyhow::Result;
|
||||
use once_cell::sync::Lazy;
|
||||
use rusqlite::{params, Connection};
|
||||
use std::path::PathBuf;
|
||||
// Removed std::sync::Mutex - using parking_lot::Mutex instead
|
||||
use tempfile::{tempdir, TempDir};
|
||||
|
||||
/// Global test database for sandbox testing
|
||||
/// Using parking_lot::Mutex which doesn't poison on panic
|
||||
use parking_lot::Mutex;
|
||||
|
||||
pub static TEST_DB: Lazy<Mutex<TestDatabase>> = Lazy::new(|| {
|
||||
Mutex::new(TestDatabase::new().expect("Failed to create test database"))
|
||||
});
|
||||
|
||||
/// Test database manager
|
||||
pub struct TestDatabase {
|
||||
pub conn: Connection,
|
||||
pub temp_dir: TempDir,
|
||||
}
|
||||
|
||||
impl TestDatabase {
|
||||
/// Create a new test database with schema
|
||||
pub fn new() -> Result<Self> {
|
||||
let temp_dir = tempdir()?;
|
||||
let db_path = temp_dir.path().join("test_sandbox.db");
|
||||
let conn = Connection::open(&db_path)?;
|
||||
|
||||
// Initialize schema
|
||||
Self::init_schema(&conn)?;
|
||||
|
||||
Ok(Self { conn, temp_dir })
|
||||
}
|
||||
|
||||
/// Initialize database schema
|
||||
fn init_schema(conn: &Connection) -> Result<()> {
|
||||
// Create sandbox profiles table
|
||||
conn.execute(
|
||||
"CREATE TABLE IF NOT EXISTS sandbox_profiles (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL UNIQUE,
|
||||
description TEXT,
|
||||
is_active BOOLEAN NOT NULL DEFAULT 0,
|
||||
is_default BOOLEAN NOT NULL DEFAULT 0,
|
||||
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
)",
|
||||
[],
|
||||
)?;
|
||||
|
||||
// Create sandbox rules table
|
||||
conn.execute(
|
||||
"CREATE TABLE IF NOT EXISTS sandbox_rules (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
profile_id INTEGER NOT NULL,
|
||||
operation_type TEXT NOT NULL,
|
||||
pattern_type TEXT NOT NULL,
|
||||
pattern_value TEXT NOT NULL,
|
||||
enabled BOOLEAN NOT NULL DEFAULT 1,
|
||||
platform_support TEXT,
|
||||
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (profile_id) REFERENCES sandbox_profiles(id) ON DELETE CASCADE
|
||||
)",
|
||||
[],
|
||||
)?;
|
||||
|
||||
// Create agents table
|
||||
conn.execute(
|
||||
"CREATE TABLE IF NOT EXISTS agents (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL,
|
||||
icon TEXT NOT NULL,
|
||||
system_prompt TEXT NOT NULL,
|
||||
default_task TEXT,
|
||||
model TEXT NOT NULL DEFAULT 'sonnet',
|
||||
sandbox_profile_id INTEGER REFERENCES sandbox_profiles(id),
|
||||
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
)",
|
||||
[],
|
||||
)?;
|
||||
|
||||
// Create agent_runs table
|
||||
conn.execute(
|
||||
"CREATE TABLE IF NOT EXISTS agent_runs (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
agent_id INTEGER NOT NULL,
|
||||
agent_name TEXT NOT NULL,
|
||||
agent_icon TEXT NOT NULL,
|
||||
task TEXT NOT NULL,
|
||||
model TEXT NOT NULL,
|
||||
project_path TEXT NOT NULL,
|
||||
output TEXT NOT NULL DEFAULT '',
|
||||
duration_ms INTEGER,
|
||||
total_tokens INTEGER,
|
||||
cost_usd REAL,
|
||||
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
completed_at TEXT,
|
||||
FOREIGN KEY (agent_id) REFERENCES agents(id) ON DELETE CASCADE
|
||||
)",
|
||||
[],
|
||||
)?;
|
||||
|
||||
// Create sandbox violations table
|
||||
conn.execute(
|
||||
"CREATE TABLE IF NOT EXISTS sandbox_violations (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
profile_id INTEGER,
|
||||
agent_id INTEGER,
|
||||
agent_run_id INTEGER,
|
||||
operation_type TEXT NOT NULL,
|
||||
pattern_value TEXT,
|
||||
process_name TEXT,
|
||||
pid INTEGER,
|
||||
denied_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (profile_id) REFERENCES sandbox_profiles(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (agent_id) REFERENCES agents(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (agent_run_id) REFERENCES agent_runs(id) ON DELETE CASCADE
|
||||
)",
|
||||
[],
|
||||
)?;
|
||||
|
||||
// Create trigger to update the updated_at timestamp for agents
|
||||
conn.execute(
|
||||
"CREATE TRIGGER IF NOT EXISTS update_agent_timestamp
|
||||
AFTER UPDATE ON agents
|
||||
FOR EACH ROW
|
||||
BEGIN
|
||||
UPDATE agents SET updated_at = CURRENT_TIMESTAMP WHERE id = NEW.id;
|
||||
END",
|
||||
[],
|
||||
)?;
|
||||
|
||||
// Create trigger to update sandbox profile timestamp
|
||||
conn.execute(
|
||||
"CREATE TRIGGER IF NOT EXISTS update_sandbox_profile_timestamp
|
||||
AFTER UPDATE ON sandbox_profiles
|
||||
FOR EACH ROW
|
||||
BEGIN
|
||||
UPDATE sandbox_profiles SET updated_at = CURRENT_TIMESTAMP WHERE id = NEW.id;
|
||||
END",
|
||||
[],
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Create a test profile with rules
|
||||
pub fn create_test_profile(&self, name: &str, rules: Vec<TestRule>) -> Result<i64> {
|
||||
// Insert profile
|
||||
self.conn.execute(
|
||||
"INSERT INTO sandbox_profiles (name, description, is_active, is_default) VALUES (?1, ?2, ?3, ?4)",
|
||||
params![name, format!("Test profile: {name}"), true, false],
|
||||
)?;
|
||||
|
||||
let profile_id = self.conn.last_insert_rowid();
|
||||
|
||||
// Insert rules
|
||||
for rule in rules {
|
||||
self.conn.execute(
|
||||
"INSERT INTO sandbox_rules (profile_id, operation_type, pattern_type, pattern_value, enabled, platform_support)
|
||||
VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
|
||||
params![
|
||||
profile_id,
|
||||
rule.operation_type,
|
||||
rule.pattern_type,
|
||||
rule.pattern_value,
|
||||
rule.enabled,
|
||||
rule.platform_support
|
||||
],
|
||||
)?;
|
||||
}
|
||||
|
||||
Ok(profile_id)
|
||||
}
|
||||
|
||||
/// Reset database to clean state
|
||||
pub fn reset(&self) -> Result<()> {
|
||||
// Delete in the correct order to respect foreign key constraints
|
||||
self.conn.execute("DELETE FROM sandbox_violations", [])?;
|
||||
self.conn.execute("DELETE FROM agent_runs", [])?;
|
||||
self.conn.execute("DELETE FROM agents", [])?;
|
||||
self.conn.execute("DELETE FROM sandbox_rules", [])?;
|
||||
self.conn.execute("DELETE FROM sandbox_profiles", [])?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Test rule structure
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct TestRule {
|
||||
pub operation_type: String,
|
||||
pub pattern_type: String,
|
||||
pub pattern_value: String,
|
||||
pub enabled: bool,
|
||||
pub platform_support: Option<String>,
|
||||
}
|
||||
|
||||
impl TestRule {
|
||||
/// Create a file read rule
|
||||
pub fn file_read(path: &str, subpath: bool) -> Self {
|
||||
Self {
|
||||
operation_type: "file_read_all".to_string(),
|
||||
pattern_type: if subpath { "subpath" } else { "literal" }.to_string(),
|
||||
pattern_value: path.to_string(),
|
||||
enabled: true,
|
||||
platform_support: Some(r#"["linux", "macos"]"#.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a network rule
|
||||
pub fn network_all() -> Self {
|
||||
Self {
|
||||
operation_type: "network_outbound".to_string(),
|
||||
pattern_type: "all".to_string(),
|
||||
pattern_value: String::new(),
|
||||
enabled: true,
|
||||
platform_support: Some(r#"["linux", "macos"]"#.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a network TCP rule
|
||||
pub fn network_tcp(port: u16) -> Self {
|
||||
Self {
|
||||
operation_type: "network_outbound".to_string(),
|
||||
pattern_type: "tcp".to_string(),
|
||||
pattern_value: port.to_string(),
|
||||
enabled: true,
|
||||
platform_support: Some(r#"["macos"]"#.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a system info read rule
|
||||
pub fn system_info_read() -> Self {
|
||||
Self {
|
||||
operation_type: "system_info_read".to_string(),
|
||||
pattern_type: "all".to_string(),
|
||||
pattern_value: String::new(),
|
||||
enabled: true,
|
||||
platform_support: Some(r#"["macos"]"#.to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Test file system structure
|
||||
pub struct TestFileSystem {
|
||||
pub root: TempDir,
|
||||
pub project_path: PathBuf,
|
||||
pub allowed_path: PathBuf,
|
||||
pub forbidden_path: PathBuf,
|
||||
}
|
||||
|
||||
impl TestFileSystem {
|
||||
/// Create a new test file system with predefined structure
|
||||
pub fn new() -> Result<Self> {
|
||||
let root = tempdir()?;
|
||||
let root_path = root.path();
|
||||
|
||||
// Create project directory
|
||||
let project_path = root_path.join("test_project");
|
||||
std::fs::create_dir_all(&project_path)?;
|
||||
|
||||
// Create allowed directory
|
||||
let allowed_path = root_path.join("allowed");
|
||||
std::fs::create_dir_all(&allowed_path)?;
|
||||
std::fs::write(allowed_path.join("test.txt"), "allowed content")?;
|
||||
|
||||
// Create forbidden directory
|
||||
let forbidden_path = root_path.join("forbidden");
|
||||
std::fs::create_dir_all(&forbidden_path)?;
|
||||
std::fs::write(forbidden_path.join("secret.txt"), "forbidden content")?;
|
||||
|
||||
// Create project files
|
||||
std::fs::write(project_path.join("main.rs"), "fn main() {}")?;
|
||||
std::fs::write(project_path.join("Cargo.toml"), "[package]\nname = \"test\"")?;
|
||||
|
||||
Ok(Self {
|
||||
root,
|
||||
project_path,
|
||||
allowed_path,
|
||||
forbidden_path,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Standard test profiles
|
||||
pub mod profiles {
|
||||
use super::*;
|
||||
|
||||
/// Minimal profile - only project access
|
||||
pub fn minimal(project_path: &str) -> Vec<TestRule> {
|
||||
vec![
|
||||
TestRule::file_read(project_path, true),
|
||||
]
|
||||
}
|
||||
|
||||
/// Standard profile - project + system libraries
|
||||
pub fn standard(project_path: &str) -> Vec<TestRule> {
|
||||
vec![
|
||||
TestRule::file_read(project_path, true),
|
||||
TestRule::file_read("/usr/lib", true),
|
||||
TestRule::file_read("/usr/local/lib", true),
|
||||
TestRule::network_all(),
|
||||
]
|
||||
}
|
||||
|
||||
/// Development profile - more permissive
|
||||
pub fn development(project_path: &str, home_dir: &str) -> Vec<TestRule> {
|
||||
vec![
|
||||
TestRule::file_read(project_path, true),
|
||||
TestRule::file_read("/usr", true),
|
||||
TestRule::file_read("/opt", true),
|
||||
TestRule::file_read(home_dir, true),
|
||||
TestRule::network_all(),
|
||||
TestRule::system_info_read(),
|
||||
]
|
||||
}
|
||||
|
||||
/// Network-only profile
|
||||
pub fn network_only() -> Vec<TestRule> {
|
||||
vec![
|
||||
TestRule::network_all(),
|
||||
]
|
||||
}
|
||||
|
||||
/// File-only profile
|
||||
pub fn file_only(paths: Vec<&str>) -> Vec<TestRule> {
|
||||
paths.into_iter()
|
||||
.map(|path| TestRule::file_read(path, true))
|
||||
.collect()
|
||||
}
|
||||
}
|
486
src-tauri/tests/sandbox/common/helpers.rs
Normal file
486
src-tauri/tests/sandbox/common/helpers.rs
Normal file
@@ -0,0 +1,486 @@
|
||||
//! Helper functions for sandbox testing
|
||||
use anyhow::{Context, Result};
|
||||
use std::env;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::{Command, Output};
|
||||
use std::time::Duration;
|
||||
|
||||
/// Check if sandboxing is supported on the current platform
|
||||
pub fn is_sandboxing_supported() -> bool {
|
||||
matches!(env::consts::OS, "linux" | "macos" | "freebsd")
|
||||
}
|
||||
|
||||
/// Skip test if sandboxing is not supported
|
||||
#[macro_export]
|
||||
macro_rules! skip_if_unsupported {
|
||||
() => {
|
||||
if !$crate::sandbox::common::is_sandboxing_supported() {
|
||||
eprintln!("Skipping test: sandboxing not supported on {}", std::env::consts::OS);
|
||||
return;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// Platform-specific test configuration
|
||||
pub struct PlatformConfig {
|
||||
pub supports_file_read: bool,
|
||||
pub supports_metadata_read: bool,
|
||||
pub supports_network_all: bool,
|
||||
pub supports_network_tcp: bool,
|
||||
pub supports_network_local: bool,
|
||||
pub supports_system_info: bool,
|
||||
}
|
||||
|
||||
impl PlatformConfig {
|
||||
/// Get configuration for current platform
|
||||
pub fn current() -> Self {
|
||||
match env::consts::OS {
|
||||
"linux" => Self {
|
||||
supports_file_read: true,
|
||||
supports_metadata_read: false, // Cannot be precisely controlled
|
||||
supports_network_all: true,
|
||||
supports_network_tcp: false, // Cannot filter by port
|
||||
supports_network_local: false, // Cannot filter by path
|
||||
supports_system_info: false,
|
||||
},
|
||||
"macos" => Self {
|
||||
supports_file_read: true,
|
||||
supports_metadata_read: true,
|
||||
supports_network_all: true,
|
||||
supports_network_tcp: true,
|
||||
supports_network_local: true,
|
||||
supports_system_info: true,
|
||||
},
|
||||
"freebsd" => Self {
|
||||
supports_file_read: false,
|
||||
supports_metadata_read: false,
|
||||
supports_network_all: false,
|
||||
supports_network_tcp: false,
|
||||
supports_network_local: false,
|
||||
supports_system_info: true, // Always allowed
|
||||
},
|
||||
_ => Self {
|
||||
supports_file_read: false,
|
||||
supports_metadata_read: false,
|
||||
supports_network_all: false,
|
||||
supports_network_tcp: false,
|
||||
supports_network_local: false,
|
||||
supports_system_info: false,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Test command builder
|
||||
pub struct TestCommand {
|
||||
command: String,
|
||||
args: Vec<String>,
|
||||
env_vars: Vec<(String, String)>,
|
||||
working_dir: Option<PathBuf>,
|
||||
}
|
||||
|
||||
impl TestCommand {
|
||||
/// Create a new test command
|
||||
pub fn new(command: &str) -> Self {
|
||||
Self {
|
||||
command: command.to_string(),
|
||||
args: Vec::new(),
|
||||
env_vars: Vec::new(),
|
||||
working_dir: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Add an argument
|
||||
pub fn arg(mut self, arg: &str) -> Self {
|
||||
self.args.push(arg.to_string());
|
||||
self
|
||||
}
|
||||
|
||||
/// Add multiple arguments
|
||||
pub fn args(mut self, args: &[&str]) -> Self {
|
||||
self.args.extend(args.iter().map(|s| s.to_string()));
|
||||
self
|
||||
}
|
||||
|
||||
/// Set an environment variable
|
||||
pub fn env(mut self, key: &str, value: &str) -> Self {
|
||||
self.env_vars.push((key.to_string(), value.to_string()));
|
||||
self
|
||||
}
|
||||
|
||||
/// Set working directory
|
||||
pub fn current_dir(mut self, dir: &Path) -> Self {
|
||||
self.working_dir = Some(dir.to_path_buf());
|
||||
self
|
||||
}
|
||||
|
||||
/// Execute the command with timeout
|
||||
pub fn execute_with_timeout(&self, timeout: Duration) -> Result<Output> {
|
||||
let mut cmd = Command::new(&self.command);
|
||||
|
||||
cmd.args(&self.args);
|
||||
|
||||
for (key, value) in &self.env_vars {
|
||||
cmd.env(key, value);
|
||||
}
|
||||
|
||||
if let Some(dir) = &self.working_dir {
|
||||
cmd.current_dir(dir);
|
||||
}
|
||||
|
||||
// On Unix, we can use a timeout mechanism
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::time::Instant;
|
||||
|
||||
let start = Instant::now();
|
||||
let mut child = cmd.spawn()
|
||||
.context("Failed to spawn command")?;
|
||||
|
||||
loop {
|
||||
match child.try_wait() {
|
||||
Ok(Some(status)) => {
|
||||
let output = child.wait_with_output()?;
|
||||
return Ok(Output {
|
||||
status,
|
||||
stdout: output.stdout,
|
||||
stderr: output.stderr,
|
||||
});
|
||||
}
|
||||
Ok(None) => {
|
||||
if start.elapsed() > timeout {
|
||||
child.kill()?;
|
||||
return Err(anyhow::anyhow!("Command timed out"));
|
||||
}
|
||||
std::thread::sleep(Duration::from_millis(100));
|
||||
}
|
||||
Err(e) => return Err(e.into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(unix))]
|
||||
{
|
||||
// Fallback for non-Unix platforms
|
||||
cmd.output()
|
||||
.context("Failed to execute command")
|
||||
}
|
||||
}
|
||||
|
||||
/// Execute and expect success
|
||||
pub fn execute_expect_success(&self) -> Result<String> {
|
||||
let output = self.execute_with_timeout(Duration::from_secs(10))?;
|
||||
|
||||
if !output.status.success() {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
return Err(anyhow::anyhow!(
|
||||
"Command failed with status {:?}. Stderr: {stderr}",
|
||||
output.status.code()
|
||||
));
|
||||
}
|
||||
|
||||
Ok(String::from_utf8_lossy(&output.stdout).to_string())
|
||||
}
|
||||
|
||||
/// Execute and expect failure
|
||||
pub fn execute_expect_failure(&self) -> Result<String> {
|
||||
let output = self.execute_with_timeout(Duration::from_secs(10))?;
|
||||
|
||||
if output.status.success() {
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
return Err(anyhow::anyhow!(
|
||||
"Command unexpectedly succeeded. Stdout: {stdout}"
|
||||
));
|
||||
}
|
||||
|
||||
Ok(String::from_utf8_lossy(&output.stderr).to_string())
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a simple test binary that attempts an operation
|
||||
pub fn create_test_binary(
|
||||
name: &str,
|
||||
code: &str,
|
||||
test_dir: &Path,
|
||||
) -> Result<PathBuf> {
|
||||
create_test_binary_with_deps(name, code, test_dir, &[])
|
||||
}
|
||||
|
||||
/// Create a test binary with optional dependencies
|
||||
pub fn create_test_binary_with_deps(
|
||||
name: &str,
|
||||
code: &str,
|
||||
test_dir: &Path,
|
||||
dependencies: &[(&str, &str)],
|
||||
) -> Result<PathBuf> {
|
||||
let src_dir = test_dir.join("src");
|
||||
std::fs::create_dir_all(&src_dir)?;
|
||||
|
||||
// Build dependencies section
|
||||
let deps_section = if dependencies.is_empty() {
|
||||
String::new()
|
||||
} else {
|
||||
let mut deps = String::from("\n[dependencies]\n");
|
||||
for (dep_name, dep_version) in dependencies {
|
||||
deps.push_str(&format!("{dep_name} = \"{dep_version}\"\n"));
|
||||
}
|
||||
deps
|
||||
};
|
||||
|
||||
// Create Cargo.toml
|
||||
let cargo_toml = format!(
|
||||
r#"[package]
|
||||
name = "{name}"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[[bin]]
|
||||
name = "{name}"
|
||||
path = "src/main.rs"
|
||||
{deps_section}"#
|
||||
);
|
||||
std::fs::write(test_dir.join("Cargo.toml"), cargo_toml)?;
|
||||
|
||||
// Create main.rs
|
||||
std::fs::write(src_dir.join("main.rs"), code)?;
|
||||
|
||||
// Build the binary
|
||||
let output = Command::new("cargo")
|
||||
.arg("build")
|
||||
.arg("--release")
|
||||
.current_dir(test_dir)
|
||||
.output()
|
||||
.context("Failed to build test binary")?;
|
||||
|
||||
if !output.status.success() {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
return Err(anyhow::anyhow!("Failed to build test binary: {stderr}"));
|
||||
}
|
||||
|
||||
let binary_path = test_dir.join("target/release").join(name);
|
||||
Ok(binary_path)
|
||||
}
|
||||
|
||||
/// Test code snippets for various operations
|
||||
pub mod test_code {
|
||||
/// Code that reads a file
|
||||
pub fn file_read(path: &str) -> String {
|
||||
format!(
|
||||
r#"
|
||||
fn main() {{
|
||||
match std::fs::read_to_string("{path}") {{
|
||||
Ok(content) => {{
|
||||
println!("SUCCESS: Read {{}} bytes", content.len());
|
||||
}}
|
||||
Err(e) => {{
|
||||
eprintln!("FAILURE: {{}}", e);
|
||||
std::process::exit(1);
|
||||
}}
|
||||
}}
|
||||
}}
|
||||
"#
|
||||
)
|
||||
}
|
||||
|
||||
/// Code that reads file metadata
|
||||
pub fn file_metadata(path: &str) -> String {
|
||||
format!(
|
||||
r#"
|
||||
fn main() {{
|
||||
match std::fs::metadata("{path}") {{
|
||||
Ok(metadata) => {{
|
||||
println!("SUCCESS: File size: {{}} bytes", metadata.len());
|
||||
}}
|
||||
Err(e) => {{
|
||||
eprintln!("FAILURE: {{}}", e);
|
||||
std::process::exit(1);
|
||||
}}
|
||||
}}
|
||||
}}
|
||||
"#
|
||||
)
|
||||
}
|
||||
|
||||
/// Code that makes a network connection
|
||||
pub fn network_connect(addr: &str) -> String {
|
||||
format!(
|
||||
r#"
|
||||
use std::net::TcpStream;
|
||||
|
||||
fn main() {{
|
||||
match TcpStream::connect("{addr}") {{
|
||||
Ok(_) => {{
|
||||
println!("SUCCESS: Connected to {addr}");
|
||||
}}
|
||||
Err(e) => {{
|
||||
eprintln!("FAILURE: {{}}", e);
|
||||
std::process::exit(1);
|
||||
}}
|
||||
}}
|
||||
}}
|
||||
"#
|
||||
)
|
||||
}
|
||||
|
||||
/// Code that reads system information
|
||||
pub fn system_info() -> &'static str {
|
||||
r#"
|
||||
#[cfg(target_os = "macos")]
|
||||
fn main() {
|
||||
use std::ffi::CString;
|
||||
use std::os::raw::c_void;
|
||||
|
||||
extern "C" {
|
||||
fn sysctlbyname(
|
||||
name: *const std::os::raw::c_char,
|
||||
oldp: *mut c_void,
|
||||
oldlenp: *mut usize,
|
||||
newp: *const c_void,
|
||||
newlen: usize,
|
||||
) -> std::os::raw::c_int;
|
||||
}
|
||||
|
||||
let name = CString::new("hw.ncpu").unwrap();
|
||||
let mut ncpu: i32 = 0;
|
||||
let mut len = std::mem::size_of::<i32>();
|
||||
|
||||
unsafe {
|
||||
let result = sysctlbyname(
|
||||
name.as_ptr(),
|
||||
&mut ncpu as *mut _ as *mut c_void,
|
||||
&mut len,
|
||||
std::ptr::null(),
|
||||
0,
|
||||
);
|
||||
|
||||
if result == 0 {
|
||||
println!("SUCCESS: CPU count: {}", ncpu);
|
||||
} else {
|
||||
eprintln!("FAILURE: sysctlbyname failed");
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
fn main() {
|
||||
println!("SUCCESS: System info test not applicable on this platform");
|
||||
}
|
||||
"#
|
||||
}
|
||||
|
||||
/// Code that tries to spawn a process
|
||||
pub fn spawn_process() -> &'static str {
|
||||
r#"
|
||||
use std::process::Command;
|
||||
|
||||
fn main() {
|
||||
match Command::new("echo").arg("test").output() {
|
||||
Ok(_) => {
|
||||
println!("SUCCESS: Spawned process");
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("FAILURE: {}", e);
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
"#
|
||||
}
|
||||
|
||||
/// Code that uses fork (requires libc)
|
||||
pub fn fork_process() -> &'static str {
|
||||
r#"
|
||||
#[cfg(unix)]
|
||||
fn main() {
|
||||
unsafe {
|
||||
let pid = libc::fork();
|
||||
if pid < 0 {
|
||||
eprintln!("FAILURE: fork failed");
|
||||
std::process::exit(1);
|
||||
} else if pid == 0 {
|
||||
// Child process
|
||||
println!("SUCCESS: Child process created");
|
||||
std::process::exit(0);
|
||||
} else {
|
||||
// Parent process
|
||||
let mut status = 0;
|
||||
libc::waitpid(pid, &mut status, 0);
|
||||
println!("SUCCESS: Fork completed");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(unix))]
|
||||
fn main() {
|
||||
eprintln!("FAILURE: fork not supported on this platform");
|
||||
std::process::exit(1);
|
||||
}
|
||||
"#
|
||||
}
|
||||
|
||||
/// Code that uses exec (requires libc)
|
||||
pub fn exec_process() -> &'static str {
|
||||
r#"
|
||||
use std::ffi::CString;
|
||||
|
||||
#[cfg(unix)]
|
||||
fn main() {
|
||||
unsafe {
|
||||
let program = CString::new("/bin/echo").unwrap();
|
||||
let arg = CString::new("test").unwrap();
|
||||
let args = vec![program.as_ptr(), arg.as_ptr(), std::ptr::null()];
|
||||
|
||||
let result = libc::execv(program.as_ptr(), args.as_ptr());
|
||||
|
||||
// If we reach here, exec failed
|
||||
eprintln!("FAILURE: exec failed with result {}", result);
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(unix))]
|
||||
fn main() {
|
||||
eprintln!("FAILURE: exec not supported on this platform");
|
||||
std::process::exit(1);
|
||||
}
|
||||
"#
|
||||
}
|
||||
|
||||
/// Code that tries to write a file
|
||||
pub fn file_write(path: &str) -> String {
|
||||
format!(
|
||||
r#"
|
||||
fn main() {{
|
||||
match std::fs::write("{path}", "test content") {{
|
||||
Ok(_) => {{
|
||||
println!("SUCCESS: Wrote file");
|
||||
}}
|
||||
Err(e) => {{
|
||||
eprintln!("FAILURE: {{}}", e);
|
||||
std::process::exit(1);
|
||||
}}
|
||||
}}
|
||||
}}
|
||||
"#
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Assert that a command output contains expected text
|
||||
pub fn assert_output_contains(output: &str, expected: &str) {
|
||||
assert!(
|
||||
output.contains(expected),
|
||||
"Expected output to contain '{expected}', but got: {output}"
|
||||
);
|
||||
}
|
||||
|
||||
/// Assert that a command output indicates success
|
||||
pub fn assert_sandbox_success(output: &str) {
|
||||
assert_output_contains(output, "SUCCESS:");
|
||||
}
|
||||
|
||||
/// Assert that a command output indicates failure
|
||||
pub fn assert_sandbox_failure(output: &str) {
|
||||
assert_output_contains(output, "FAILURE:");
|
||||
}
|
8
src-tauri/tests/sandbox/common/mod.rs
Normal file
8
src-tauri/tests/sandbox/common/mod.rs
Normal file
@@ -0,0 +1,8 @@
|
||||
//! Common test utilities and helpers for sandbox testing
|
||||
pub mod fixtures;
|
||||
pub mod helpers;
|
||||
pub mod claude_real;
|
||||
|
||||
pub use fixtures::*;
|
||||
pub use helpers::*;
|
||||
pub use claude_real::*;
|
Reference in New Issue
Block a user