use std::io::{Read, Write};
use std::process::{Command, Stdio};
use std::time::{Duration, Instant};
fn get_binary_path() -> String {
if let Ok(path) = std::env::var("SELFWARE_BINARY") {
return path;
}
env!("CARGO_BIN_EXE_selfware").to_string()
}
fn run_interactive(input: &str, timeout_secs: u64) -> (String, String, i32) {
let binary = get_binary_path();
let mut child = Command::new(&binary)
.arg("chat")
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.expect("Failed to spawn selfware");
if let Some(mut stdin) = child.stdin.take() {
stdin.write_all(input.as_bytes()).ok();
stdin.write_all(b"\n").ok();
}
let timeout = Duration::from_secs(timeout_secs);
let start = Instant::now();
let poll_interval = Duration::from_millis(100);
loop {
match child.try_wait() {
Ok(Some(status)) => {
let mut stdout_buf = Vec::new();
let mut stderr_buf = Vec::new();
if let Some(mut stdout) = child.stdout.take() {
stdout.read_to_end(&mut stdout_buf).ok();
}
if let Some(mut stderr) = child.stderr.take() {
stderr.read_to_end(&mut stderr_buf).ok();
}
let stdout = String::from_utf8_lossy(&stdout_buf).to_string();
let stderr = String::from_utf8_lossy(&stderr_buf).to_string();
let code = status.code().unwrap_or(-1);
return (stdout, stderr, code);
}
Ok(None) => {
if start.elapsed() >= timeout {
child.kill().ok();
let output = child.wait_with_output().expect("Failed to wait after kill");
let stdout = String::from_utf8_lossy(&output.stdout).to_string();
let stderr = String::from_utf8_lossy(&output.stderr).to_string();
return (stdout, format!("timeout: {}", stderr), -1);
}
std::thread::sleep(poll_interval);
}
Err(_) => {
return ("".to_string(), "process error".to_string(), -1);
}
}
}
}
fn run_task(task: &str, timeout_secs: u64) -> (String, String, i32) {
let mut child = Command::new(get_binary_path())
.args(["--yolo", "run", task])
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.expect("Failed to spawn selfware");
let timeout = Duration::from_secs(timeout_secs);
let start = Instant::now();
let poll_interval = Duration::from_millis(100);
loop {
match child.try_wait() {
Ok(Some(status)) => {
let mut stdout_buf = Vec::new();
let mut stderr_buf = Vec::new();
if let Some(mut stdout) = child.stdout.take() {
stdout.read_to_end(&mut stdout_buf).ok();
}
if let Some(mut stderr) = child.stderr.take() {
stderr.read_to_end(&mut stderr_buf).ok();
}
let stdout = String::from_utf8_lossy(&stdout_buf).to_string();
let stderr = String::from_utf8_lossy(&stderr_buf).to_string();
let code = status.code().unwrap_or(-1);
return (stdout, stderr, code);
}
Ok(None) => {
if start.elapsed() >= timeout {
child.kill().ok();
child.wait().ok();
return ("".to_string(), "timeout".to_string(), -1);
}
std::thread::sleep(poll_interval);
}
Err(_) => {
return ("".to_string(), "process error".to_string(), -1);
}
}
}
}
#[test]
#[cfg(feature = "integration")]
fn test_interactive_help_command() {
let (stdout, _stderr, _code) = run_interactive("/help\nexit\n", 30);
assert!(
stdout.contains("/help") || stdout.contains("Commands:"),
"Should display help. Got: {}",
stdout
);
}
#[test]
#[cfg(feature = "integration")]
fn test_interactive_status_command() {
let (stdout, _stderr, _code) = run_interactive("/status\nexit\n", 30);
assert!(
stdout.contains("Messages") || stdout.contains("Memory") || stdout.contains("tokens"),
"Should display status. Got: {}",
stdout
);
}
#[test]
#[cfg(feature = "integration")]
fn test_interactive_memory_command() {
let (stdout, _stderr, _code) = run_interactive("/memory\nexit\n", 30);
assert!(
stdout.contains("Memory") || stdout.contains("tokens") || stdout.contains("entries"),
"Should display memory stats. Got: {}",
stdout
);
}
#[test]
#[cfg(feature = "integration")]
fn test_interactive_clear_command() {
let (stdout, _stderr, _code) = run_interactive("/clear\nexit\n", 30);
assert!(
stdout.contains("clear") || stdout.contains("Clear"),
"Should confirm clearing. Got: {}",
stdout
);
}
#[test]
#[cfg(feature = "integration")]
fn test_interactive_tools_command() {
let (stdout, stderr, _code) = run_interactive("/tools\nexit\n", 30);
let combined = format!("{}{}", stdout, stderr);
assert!(
combined.contains("file_read") || combined.contains("directory_tree"),
"Should list tools. Got stdout: {}, stderr: {}",
stdout,
stderr
);
}
#[test]
#[cfg(feature = "integration")]
fn test_interactive_debug_command_without_checkpoint() {
let (stdout, stderr, _code) = run_interactive("/debug\nexit\n", 30);
let combined = format!("{}{}", stdout, stderr);
assert!(
combined.contains("Execution Debug"),
"Should show debug header. stdout: {}, stderr: {}",
stdout,
stderr
);
assert!(
combined.contains("No active checkpoint/tool history for this session."),
"Should explain there is no task history yet. stdout: {}, stderr: {}",
stdout,
stderr
);
}
#[test]
#[cfg(feature = "integration")]
fn test_interactive_extended_debug_commands_without_checkpoint() {
let (stdout, stderr, _code) = run_interactive(
"/debug full\n/debug tool 1\n/debug state\n/debug-log full\nexit\n",
30,
);
let combined = format!("{}{}", stdout, stderr);
assert!(
combined.contains("Execution Debug"),
"Should show execution debug output. stdout: {}, stderr: {}",
stdout,
stderr
);
assert!(
combined.contains("No active checkpoint/tool history for this session."),
"Should explain missing task history. stdout: {}, stderr: {}",
stdout,
stderr
);
assert!(
combined.contains("Session Debug Log"),
"Should show session log output. stdout: {}, stderr: {}",
stdout,
stderr
);
assert!(
combined.contains("Task State"),
"Should show task-state debug output. stdout: {}, stderr: {}",
stdout,
stderr
);
}
#[test]
#[cfg(feature = "integration")]
fn test_interactive_queue_commands() {
let input = concat!(
"/queue first queued task\n",
"/queue second queued task\n",
"/queue list\n",
"/queue drop 1\n",
"/queue list\n",
"/queue clear\n",
"/queue list\n",
"exit\n"
);
let (stdout, stderr, _code) = run_interactive(input, 30);
let combined = format!("{}{}", stdout, stderr);
assert!(
combined.contains("Queued (1 pending)") && combined.contains("Queued (2 pending)"),
"Should acknowledge queued messages. stdout: {}, stderr: {}",
stdout,
stderr
);
assert!(
combined.contains("Queued messages (2):"),
"Should list both queued messages. stdout: {}, stderr: {}",
stdout,
stderr
);
assert!(
combined.contains("Removed message 1: first queued task"),
"Should drop the first queued message. stdout: {}, stderr: {}",
stdout,
stderr
);
assert!(
combined.contains("Queued messages (1):") && combined.contains("second queued task"),
"Should keep the remaining queued message after drop. stdout: {}, stderr: {}",
stdout,
stderr
);
assert!(
combined.contains("Cleared 1 queued message(s).") && combined.contains("Queue is empty."),
"Should clear the queue and report empty state. stdout: {}, stderr: {}",
stdout,
stderr
);
}
#[test]
#[cfg(feature = "integration")]
fn test_interactive_exit_command() {
let (stdout, _stderr, code) = run_interactive("exit\n", 30);
assert!(
code == 0 || stdout.contains("exit") || stdout.contains("Basic Mode"),
"Should exit. Code: {}, stdout: {}",
code,
stdout
);
}
#[test]
#[cfg(feature = "integration")]
fn test_interactive_quit_command() {
let (stdout, _stderr, code) = run_interactive("quit\n", 30);
assert!(
code == 0 || stdout.contains("quit") || stdout.contains("Basic Mode"),
"Should quit. Code: {}, stdout: {}",
code,
stdout
);
}
#[test]
#[cfg(feature = "integration")]
fn test_interactive_fallback_to_basic_mode() {
let (stdout, stderr, _code) = run_interactive("exit\n", 30);
assert!(
stdout.contains("Basic Mode")
|| stderr.contains("basic mode")
|| stderr.contains("falling back"),
"Should fall back to basic mode. stdout: {}, stderr: {}",
stdout,
stderr
);
}
#[test]
#[cfg(feature = "integration")]
fn test_run_command_simple_task() {
let (stdout, stderr, code) = run_task("echo hello", 60);
let combined = format!("{}{}", stdout, stderr);
assert!(
code == 0
|| combined.contains("Task")
|| combined.contains("completed")
|| combined.contains("Tool")
|| combined.contains("hello")
|| combined.contains("timeout")
|| combined.contains("timed out")
|| code == -1,
"Should run task or timeout gracefully. code: {}, stdout: {}, stderr: {}",
code,
stdout,
stderr
);
}
#[test]
#[cfg(feature = "integration")]
fn test_analyze_command() {
let output = Command::new(get_binary_path())
.args(["analyze", "./src"])
.output()
.expect("Failed to run selfware");
let stdout = String::from_utf8_lossy(&output.stdout).to_string();
assert!(
stdout.contains("Surveying") || stdout.contains("directory") || stdout.contains("Tool"),
"Should analyze directory. Got: {}",
stdout
);
}
#[test]
#[cfg(feature = "integration")]
fn test_help_flag() {
let output = Command::new(get_binary_path())
.arg("--help")
.output()
.expect("Failed to run selfware");
let stdout = String::from_utf8_lossy(&output.stdout).to_string();
assert!(
stdout.contains("Usage:") || stdout.contains("selfware"),
"Should show help. Got: {}",
stdout
);
assert!(stdout.contains("chat"), "Should list chat command");
assert!(stdout.contains("run"), "Should list run command");
}
#[test]
#[cfg(feature = "integration")]
fn test_version_flag() {
let output = Command::new(get_binary_path())
.arg("--version")
.output()
.expect("Failed to run selfware");
let stdout = String::from_utf8_lossy(&output.stdout).to_string();
assert!(
stdout.contains("selfware") || stdout.contains("0."),
"Should show version. Got: {}",
stdout
);
}
#[test]
#[cfg(feature = "integration")]
fn test_journal_command() {
let output = Command::new(get_binary_path())
.arg("journal")
.output()
.expect("Failed to run selfware");
let stdout = String::from_utf8_lossy(&output.stdout).to_string();
let code = output.status.code().unwrap_or(-1);
assert!(
code == 0 || stdout.contains("journal") || stdout.contains("No"),
"Should handle journal. Code: {}, stdout: {}",
code,
stdout
);
}
#[test]
#[cfg(feature = "integration")]
fn test_status_command() {
let output = Command::new(get_binary_path())
.arg("status")
.output()
.expect("Failed to run selfware");
let stdout = String::from_utf8_lossy(&output.stdout).to_string();
assert!(
stdout.contains("WORKSHOP") || stdout.contains("status") || stdout.contains("Status"),
"Should show status. Got: {}",
stdout
);
}
#[test]
#[cfg(feature = "integration")]
fn test_garden_command() {
let output = Command::new(get_binary_path())
.args(["garden", "."])
.output()
.expect("Failed to run selfware");
let stdout = String::from_utf8_lossy(&output.stdout).to_string();
let code = output.status.code().unwrap_or(-1);
assert!(
code == 0 || stdout.contains("garden") || stdout.contains("Garden"),
"Should show garden. Code: {}, stdout: {}",
code,
stdout
);
}
#[test]
#[cfg(feature = "integration")]
fn test_multi_chat_init() {
let mut child = Command::new(get_binary_path())
.args(["multi-chat", "-n", "2"])
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.expect("Failed to spawn selfware");
if let Some(mut stdin) = child.stdin.take() {
stdin.write_all(b"exit\n").ok();
}
let output = child.wait_with_output().expect("Failed to wait");
let stdout = String::from_utf8_lossy(&output.stdout).to_string();
assert!(
stdout.contains("concurrent") || stdout.contains("Multi") || stdout.contains("WORKSHOP"),
"Should init multi-chat. Got: {}",
stdout
);
}
#[test]
#[cfg(feature = "integration")]
fn test_config_flag() {
let output = Command::new(get_binary_path())
.args(["-c", "selfware.toml", "--help"])
.output()
.expect("Failed to run selfware");
let code = output.status.code().unwrap_or(-1);
assert!(code == 0, "Should accept config flag");
}
#[test]
#[cfg(feature = "integration")]
fn test_workdir_flag() {
let tmp = std::env::temp_dir();
let tmp_str = tmp.to_string_lossy();
let output = Command::new(get_binary_path())
.args(["-C", &tmp_str, "--help"])
.output()
.expect("Failed to run selfware");
let code = output.status.code().unwrap_or(-1);
assert!(code == 0, "Should accept workdir flag");
}
#[test]
#[cfg(feature = "integration")]
fn test_invalid_command() {
let output = Command::new(get_binary_path())
.arg("invalid_command_xyz")
.output()
.expect("Failed to run selfware");
let stderr = String::from_utf8_lossy(&output.stderr).to_string();
let code = output.status.code().unwrap_or(-1);
assert!(
code != 0 || stderr.contains("error") || stderr.contains("invalid"),
"Should reject invalid command. Code: {}",
code
);
}
#[test]
#[cfg(feature = "integration")]
fn test_quiet_mode() {
let output = Command::new(get_binary_path())
.args(["-q", "status"])
.output()
.expect("Failed to run selfware");
let stdout = String::from_utf8_lossy(&output.stdout).to_string();
let _ = stdout;
}
#[test]
#[cfg(feature = "integration")]
fn test_interrupt_handling() {
let mut child = Command::new(get_binary_path())
.arg("chat")
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.expect("Failed to spawn selfware");
std::thread::sleep(Duration::from_millis(500));
child.kill().ok();
let status = child.wait().expect("Failed to wait");
assert!(
!status.success() || status.code().is_some(),
"Process should be killable"
);
}
#[test]
#[cfg(feature = "integration")]
fn test_binary_exists() {
let binary_path = get_binary_path();
let path = std::path::Path::new(&binary_path);
assert!(
path.exists(),
"Binary should exist at {} (run: cargo test to build)",
binary_path
);
}
#[test]
#[cfg(feature = "integration")]
fn test_env_var_config() {
let output = Command::new(get_binary_path())
.env("SELFWARE_DEBUG", "1")
.arg("--help")
.output()
.expect("Failed to run selfware");
let code = output.status.code().unwrap_or(-1);
assert!(code == 0, "Should work with env var");
}
#[test]
#[cfg(feature = "integration")]
fn test_output_format_json() {
let output = Command::new(get_binary_path())
.args(["status", "--output-format", "json"])
.output()
.expect("Failed to run selfware");
let stdout = String::from_utf8_lossy(&output.stdout).to_string();
let code = output.status.code().unwrap_or(-1);
assert!(code == 0, "Should exit successfully");
let parsed: Result<serde_json::Value, _> = serde_json::from_str(&stdout);
assert!(
parsed.is_ok(),
"Output should be valid JSON. Got: {}",
stdout
);
let json = parsed.unwrap();
assert!(
json.get("model").is_some(),
"JSON should have 'model' field"
);
assert!(
json.get("journal").is_some(),
"JSON should have 'journal' field"
);
}
#[test]
#[cfg(feature = "integration")]
fn test_no_color_flag() {
let output = Command::new(get_binary_path())
.args(["--no-color", "status"])
.output()
.expect("Failed to run selfware");
let stdout = String::from_utf8_lossy(&output.stdout).to_string();
assert!(
!stdout.contains("\x1b["),
"Output should not contain ANSI escape codes with --no-color"
);
}
#[test]
#[cfg(feature = "integration")]
fn test_no_color_env_var() {
let output = Command::new(get_binary_path())
.env("NO_COLOR", "1")
.arg("status")
.output()
.expect("Failed to run selfware");
let stdout = String::from_utf8_lossy(&output.stdout).to_string();
assert!(
!stdout.contains("\x1b["),
"Output should not contain ANSI escape codes with NO_COLOR env var"
);
}
#[test]
#[cfg(feature = "integration")]
fn test_selfware_timeout_env_var() {
let output = Command::new(get_binary_path())
.env("SELFWARE_TIMEOUT", "120")
.arg("--help")
.output()
.expect("Failed to run selfware");
let code = output.status.code().unwrap_or(-1);
assert!(code == 0, "Should accept SELFWARE_TIMEOUT env var");
}
#[test]
#[ignore] #[cfg(feature = "integration")]
fn test_non_interactive_fails_fast_on_confirmation() {
let mut child = Command::new(get_binary_path())
.args(["run", "use shell_exec to run pwd"])
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.expect("Failed to spawn selfware");
drop(child.stdin.take());
let timeout = Duration::from_secs(60);
let start = Instant::now();
loop {
match child.try_wait() {
Ok(Some(status)) => {
let mut stdout_buf = Vec::new();
let mut stderr_buf = Vec::new();
if let Some(mut stdout) = child.stdout.take() {
use std::io::Read;
stdout.read_to_end(&mut stdout_buf).ok();
}
if let Some(mut stderr) = child.stderr.take() {
use std::io::Read;
stderr.read_to_end(&mut stderr_buf).ok();
}
let stdout = String::from_utf8_lossy(&stdout_buf);
let stderr = String::from_utf8_lossy(&stderr_buf);
let combined = format!("{}{}", stdout, stderr);
assert!(
!status.success(),
"Non-interactive mode should fail when confirmation required"
);
assert!(
combined.contains("confirmation")
|| combined.contains("non-interactive")
|| combined.contains("--yolo"),
"Error should mention confirmation issue. Output: {}",
combined
);
let recovery_count = combined.matches("Recovering from error").count();
assert!(
recovery_count == 0,
"Should fail immediately without recovery loop, but found {} recovery attempts",
recovery_count
);
return;
}
Ok(None) => {
if start.elapsed() >= timeout {
child.kill().ok();
panic!("Test timed out - possible infinite loop in error handling");
}
std::thread::sleep(Duration::from_millis(100));
}
Err(e) => panic!("Error waiting for process: {}", e),
}
}
}