use base64::{engine::general_purpose, Engine};
use serde_json::json;
use std::sync::Arc;
use tempfile::TempDir;
use tokio::sync::Mutex;
use winx_code_agent::errors::{Result, WinxError};
use winx_code_agent::state::bash_state::BashState;
use winx_code_agent::types::{
BashCommand, BashCommandAction, ContextSave, FileWriteOrEdit, Initialize, InitializeType,
ModeName, ReadFiles,
};
use winx_code_agent::WinxService;
#[test]
fn test_winx_service_creation() {
let service = WinxService::new();
assert!(!service.version.is_empty());
}
#[test]
fn test_winx_service_default() {
let service = WinxService::default();
assert!(!service.version.is_empty());
}
#[tokio::test]
async fn test_initialize_first_call_wcgw_mode() -> Result<()> {
let temp_dir = TempDir::new()?;
let bash_state_arc: Arc<Mutex<Option<BashState>>> = Arc::new(Mutex::new(None));
let init = Initialize {
init_type: InitializeType::FirstCall,
mode_name: ModeName::Wcgw,
any_workspace_path: temp_dir.path().to_string_lossy().to_string(),
thread_id: String::new(),
code_writer_config: None,
initial_files_to_read: vec![],
task_id_to_resume: String::new(),
};
let response =
winx_code_agent::tools::initialize::handle_tool_call(&bash_state_arc, init).await?;
assert!(response.contains("Initialized"));
assert!(response.contains("thread_id"));
assert!(response.contains("temporary_artifact_dir="));
assert!(response.contains(".winx/tmp/session-"));
assert!(
!temp_dir.path().join(".winx/tmp").exists(),
"Initialize should advertise the path without polluting an unused workspace"
);
let state = bash_state_arc.lock().await;
assert!(state.is_some());
let bash_state = state.as_ref().ok_or(WinxError::BashStateNotInitialized)?;
assert!(bash_state.initialized);
assert!(!bash_state.current_thread_id.is_empty());
Ok(())
}
#[tokio::test]
async fn test_initialize_loads_agent_guidelines() -> Result<()> {
let temp_dir = TempDir::new()?;
std::fs::write(temp_dir.path().join("AGENTS.md"), "Use repo-specific instructions.\n")?;
let bash_state_arc: Arc<Mutex<Option<BashState>>> = Arc::new(Mutex::new(None));
let init = Initialize {
init_type: InitializeType::FirstCall,
mode_name: ModeName::Wcgw,
any_workspace_path: temp_dir.path().to_string_lossy().to_string(),
thread_id: "guidelines-test".to_string(),
code_writer_config: None,
initial_files_to_read: vec![],
task_id_to_resume: String::new(),
};
let response =
winx_code_agent::tools::initialize::handle_tool_call(&bash_state_arc, init).await?;
assert!(response.contains("# Agent guidelines"));
assert!(response.contains("Use repo-specific instructions."));
Ok(())
}
#[tokio::test]
async fn repeated_first_call_is_compact_and_preserves_the_active_mode() -> Result<()> {
let temp_dir = TempDir::new()?;
std::fs::write(temp_dir.path().join("AGENTS.md"), "large context marker\n".repeat(200))?;
let bash_state_arc: Arc<Mutex<Option<BashState>>> = Arc::new(Mutex::new(None));
let thread_id = "compact-repeat";
let first = winx_code_agent::tools::initialize::handle_tool_call(
&bash_state_arc,
Initialize {
init_type: InitializeType::FirstCall,
mode_name: ModeName::Wcgw,
any_workspace_path: temp_dir.path().to_string_lossy().into_owned(),
thread_id: thread_id.to_string(),
code_writer_config: None,
initial_files_to_read: vec![],
task_id_to_resume: String::new(),
},
)
.await?;
assert!(first.contains("large context marker"), "{first}");
let repeated = winx_code_agent::tools::initialize::handle_tool_call(
&bash_state_arc,
Initialize {
init_type: InitializeType::FirstCall,
mode_name: ModeName::CodeWriter,
any_workspace_path: temp_dir.path().join("ignored").to_string_lossy().into_owned(),
thread_id: thread_id.to_string(),
code_writer_config: None,
initial_files_to_read: vec![],
task_id_to_resume: String::new(),
},
)
.await?;
assert!(repeated.contains("Context and instructions are unchanged"), "{repeated}");
assert!(repeated.contains("temporary_artifact_dir="), "{repeated}");
assert!(!repeated.contains("large context marker"), "{repeated}");
assert!(!repeated.contains("# Winx orchestration contract"), "{repeated}");
assert!(repeated.len() * 4 < first.len(), "first={} repeated={}", first.len(), repeated.len());
assert!(!temp_dir.path().join("ignored").exists());
let state = bash_state_arc.lock().await;
assert!(matches!(
state.as_ref().map(|state| state.mode),
Some(winx_code_agent::types::Modes::Wcgw)
));
Ok(())
}
#[tokio::test]
async fn test_context_save_resume_restores_task_context() -> Result<()> {
let temp_dir = TempDir::new()?;
let bash_state_arc: Arc<Mutex<Option<BashState>>> = Arc::new(Mutex::new(None));
let task_id = format!("resume-test-{}", std::process::id());
let init = Initialize {
init_type: InitializeType::FirstCall,
mode_name: ModeName::Wcgw,
any_workspace_path: temp_dir.path().to_string_lossy().to_string(),
thread_id: "resumesource".to_string(),
code_writer_config: None,
initial_files_to_read: vec![],
task_id_to_resume: String::new(),
};
winx_code_agent::tools::initialize::handle_tool_call(&bash_state_arc, init).await?;
let context = ContextSave {
id: task_id.clone(),
project_root_path: temp_dir.path().to_string_lossy().to_string(),
description: "resume payload marker".to_string(),
relevant_file_globs: vec![],
thread_id: String::new(),
};
winx_code_agent::tools::context_save::handle_tool_call(&bash_state_arc, context).await?;
let resumed_state_arc: Arc<Mutex<Option<BashState>>> = Arc::new(Mutex::new(None));
let resume_init = Initialize {
init_type: InitializeType::FirstCall,
mode_name: ModeName::Wcgw,
any_workspace_path: temp_dir.path().to_string_lossy().to_string(),
thread_id: "resumetarget".to_string(),
code_writer_config: None,
initial_files_to_read: vec![],
task_id_to_resume: task_id,
};
let response =
winx_code_agent::tools::initialize::handle_tool_call(&resumed_state_arc, resume_init)
.await?;
assert!(response.contains("# Resumed task"));
assert!(response.contains("resume payload marker"));
let state = resumed_state_arc.lock().await;
let state = state.as_ref().ok_or(WinxError::BashStateNotInitialized)?;
assert_eq!(state.current_thread_id, "resumetarget");
assert_eq!(state.workspace_root, std::fs::canonicalize(temp_dir.path())?);
Ok(())
}
#[tokio::test]
async fn test_initialize_architect_mode() -> Result<()> {
let temp_dir = TempDir::new()?;
let bash_state_arc: Arc<Mutex<Option<BashState>>> = Arc::new(Mutex::new(None));
let init = Initialize {
init_type: InitializeType::FirstCall,
mode_name: ModeName::Architect,
any_workspace_path: temp_dir.path().to_string_lossy().to_string(),
thread_id: "architectreadonly".to_string(),
code_writer_config: None,
initial_files_to_read: vec![],
task_id_to_resume: String::new(),
};
let response =
winx_code_agent::tools::initialize::handle_tool_call(&bash_state_arc, init).await?;
assert!(response.contains("Initialized"));
let state = bash_state_arc.lock().await;
let bash_state = state.as_ref().ok_or(WinxError::BashStateNotInitialized)?;
assert!(bash_state.initialized);
drop(state);
let marker = temp_dir.path().join("architect-must-not-write");
let denied = BashCommand {
action_json: BashCommandAction::Command {
command: format!("touch {}", marker.display()),
is_background: false,
allow_multi: false,
},
wait_for_seconds: Some(0.2),
thread_id: "architectreadonly".to_string(),
};
let result =
winx_code_agent::tools::bash_command::handle_tool_call(&bash_state_arc, denied).await;
assert!(matches!(result, Err(WinxError::CommandNotAllowed(_))));
assert!(!marker.exists(), "architect mode must not mutate the workspace");
let redirect_denied = BashCommand {
action_json: BashCommandAction::Command {
command: format!("echo bypass > {}", marker.display()),
is_background: false,
allow_multi: false,
},
wait_for_seconds: Some(0.2),
thread_id: "architectreadonly".to_string(),
};
let result =
winx_code_agent::tools::bash_command::handle_tool_call(&bash_state_arc, redirect_denied)
.await;
assert!(matches!(result, Err(WinxError::CommandNotAllowed(_))));
assert!(!marker.exists(), "architect mode must reject shell output redirects");
let allowed = BashCommand {
action_json: BashCommandAction::Command {
command: "pwd".to_string(),
is_background: false,
allow_multi: false,
},
wait_for_seconds: Some(1.0),
thread_id: "architectreadonly".to_string(),
};
let output =
winx_code_agent::tools::bash_command::handle_tool_call(&bash_state_arc, allowed).await?;
assert!(output.contains(&temp_dir.path().canonicalize()?.display().to_string()));
Ok(())
}
#[tokio::test]
async fn test_initialize_with_file_path() -> Result<()> {
let temp_dir = TempDir::new()?;
let file_path = temp_dir.path().join("test.txt");
std::fs::write(&file_path, "test content")?;
let bash_state_arc: Arc<Mutex<Option<BashState>>> = Arc::new(Mutex::new(None));
let init = Initialize {
init_type: InitializeType::FirstCall,
mode_name: ModeName::Wcgw,
any_workspace_path: file_path.to_string_lossy().to_string(),
thread_id: String::new(),
code_writer_config: None,
initial_files_to_read: vec![],
task_id_to_resume: String::new(),
};
let response =
winx_code_agent::tools::initialize::handle_tool_call(&bash_state_arc, init).await?;
assert!(response.contains("parent directory") || response.contains("Initialized"));
Ok(())
}
#[tokio::test]
async fn test_initialize_mode_change() -> Result<()> {
let temp_dir = TempDir::new()?;
let bash_state_arc: Arc<Mutex<Option<BashState>>> = Arc::new(Mutex::new(None));
let init = Initialize {
init_type: InitializeType::FirstCall,
mode_name: ModeName::Wcgw,
any_workspace_path: temp_dir.path().to_string_lossy().to_string(),
thread_id: String::new(),
code_writer_config: None,
initial_files_to_read: vec![],
task_id_to_resume: String::new(),
};
winx_code_agent::tools::initialize::handle_tool_call(&bash_state_arc, init).await?;
let thread_id = {
let state = bash_state_arc.lock().await;
state.as_ref().ok_or(WinxError::BashStateNotInitialized)?.current_thread_id.clone()
};
let mode_change = Initialize {
init_type: InitializeType::UserAskedModeChange,
mode_name: ModeName::Architect,
any_workspace_path: temp_dir.path().to_string_lossy().to_string(),
thread_id,
code_writer_config: None,
initial_files_to_read: vec![],
task_id_to_resume: String::new(),
};
let response =
winx_code_agent::tools::initialize::handle_tool_call(&bash_state_arc, mode_change).await?;
assert!(response.contains("Changed mode"));
Ok(())
}
#[tokio::test]
async fn test_initialize_normalizes_thread_id() -> Result<()> {
let temp_dir = TempDir::new()?;
let bash_state_arc: Arc<Mutex<Option<BashState>>> = Arc::new(Mutex::new(None));
let init = Initialize {
init_type: InitializeType::FirstCall,
mode_name: ModeName::Wcgw,
any_workspace_path: temp_dir.path().to_string_lossy().to_string(),
thread_id: "thread-123_$".to_string(),
code_writer_config: None,
initial_files_to_read: vec![],
task_id_to_resume: String::new(),
};
winx_code_agent::tools::initialize::handle_tool_call(&bash_state_arc, init).await?;
let state = bash_state_arc.lock().await;
let bash_state = state.as_ref().ok_or(WinxError::BashStateNotInitialized)?;
assert!(
bash_state.current_thread_id.starts_with("thread123_")
&& bash_state.current_thread_id != "thread123_",
"got {}",
bash_state.current_thread_id
);
Ok(())
}
#[tokio::test]
async fn test_initialize_requires_thread_id_after_first_call() -> Result<()> {
let temp_dir = TempDir::new()?;
let bash_state_arc: Arc<Mutex<Option<BashState>>> = Arc::new(Mutex::new(None));
let mode_change = Initialize {
init_type: InitializeType::UserAskedModeChange,
mode_name: ModeName::Architect,
any_workspace_path: temp_dir.path().to_string_lossy().to_string(),
thread_id: String::new(),
code_writer_config: None,
initial_files_to_read: vec![],
task_id_to_resume: String::new(),
};
let result =
winx_code_agent::tools::initialize::handle_tool_call(&bash_state_arc, mode_change).await;
assert!(matches!(result, Err(WinxError::ThreadIdMismatch(_))));
Ok(())
}
#[test]
fn test_read_files_deserializes_wcgw_line_ranges(
) -> std::result::Result<(), Box<dyn std::error::Error>> {
let plain_path = "/tmp/example.txt";
let colon_path = "/tmp/example.txt:colon_in_name";
let url_like_path = "/path/to/http://example.com/file.txt";
let read: ReadFiles = serde_json::from_value(json!({
"file_paths": [
plain_path,
format!("{plain_path}:2"),
format!("{plain_path}:-3"),
format!("{plain_path}:2-4"),
format!("{plain_path}:5-"),
format!("{plain_path}:invalid-line"),
colon_path,
url_like_path,
format!("{url_like_path}:10-20")
]
}))?;
assert_eq!(
read.file_paths,
vec![
plain_path,
plain_path,
plain_path,
plain_path,
plain_path,
"/tmp/example.txt:invalid-line",
colon_path,
url_like_path,
url_like_path,
]
);
assert_eq!(
read.start_line_nums,
vec![None, Some(2), None, Some(2), Some(5), None, None, None, Some(10),]
);
assert_eq!(
read.end_line_nums,
vec![None, None, Some(3), Some(4), None, None, None, None, Some(20)]
);
Ok(())
}
#[test]
fn test_bash_command_deserializer_normalizes_thread_id_and_preserves_tail(
) -> std::result::Result<(), Box<dyn std::error::Error>> {
let bash_command: BashCommand = serde_json::from_value(json!({
"action_json": {
"type": "command",
"command": "rg TODO src | tail -n 20"
},
"thread_id": "thread-123_$"
}))?;
assert!(
bash_command.thread_id.starts_with("thread123_") && bash_command.thread_id != "thread123_",
"got {}",
bash_command.thread_id
);
if let BashCommandAction::Command { command, .. } = bash_command.action_json {
assert_eq!(command, "rg TODO src | tail -n 20");
} else {
return Err("expected command action".into());
}
Ok(())
}
#[test]
fn test_bash_command_deserializer_accepts_command_shorthand(
) -> std::result::Result<(), Box<dyn std::error::Error>> {
let bash_command: BashCommand = serde_json::from_value(json!({
"action_json": {
"command": "pwd",
"cwd": "/tmp"
},
"thread_id": "thread-123_$"
}))?;
assert!(
bash_command.thread_id.starts_with("thread123_") && bash_command.thread_id != "thread123_",
"got {}",
bash_command.thread_id
);
if let BashCommandAction::Command { command, allow_multi, .. } = bash_command.action_json {
assert_eq!(command, "pwd");
assert!(!allow_multi);
} else {
return Err("expected command action".into());
}
Ok(())
}
#[test]
fn test_bash_command_deserializer_accepts_top_level_command_shorthand(
) -> std::result::Result<(), Box<dyn std::error::Error>> {
let bash_command: BashCommand = serde_json::from_value(json!({
"command": "pwd\nls",
"wait_for_seconds": 2,
"thread_id": "thread-123_$"
}))?;
assert!(
bash_command.thread_id.starts_with("thread123_") && bash_command.thread_id != "thread123_",
"got {}",
bash_command.thread_id
);
assert_eq!(bash_command.wait_for_seconds, Some(2.0));
if let BashCommandAction::Command { command, allow_multi, .. } = bash_command.action_json {
assert_eq!(command, "pwd\nls");
assert!(!allow_multi);
} else {
return Err("expected command action".into());
}
Ok(())
}
#[test]
fn test_bash_wait_policy_is_optional_and_accepts_all_modes(
) -> std::result::Result<(), Box<dyn std::error::Error>> {
let legacy: BashCommand = serde_json::from_value(json!({ "command": "pwd" }))?;
assert_eq!(legacy.wait_for_seconds, None);
for wire in ["adaptive", "return_early", "until_complete"] {
let parsed: BashCommand = serde_json::from_value(json!({
"command": "pwd",
"wait_policy": wire
}))?;
assert!(matches!(parsed.action_json, BashCommandAction::Command { .. }));
assert!(serde_json::to_value(parsed)?.get("wait_policy").is_none());
}
assert!(serde_json::from_value::<BashCommand>(json!({
"command": "pwd",
"wait_policy": "invalid"
}))
.is_err());
Ok(())
}
#[test]
fn test_bash_command_deserializer_sanitizes_json_nul_escape(
) -> std::result::Result<(), Box<dyn std::error::Error>> {
let command_with_nul = format!("printf '{}'", '\0');
let bash_command: BashCommand = serde_json::from_value(json!({
"action_json": {
"command": command_with_nul
},
"thread_id": "thread-123_$"
}))?;
if let BashCommandAction::Command { command, .. } = bash_command.action_json {
assert_eq!(command, "printf '\\x00'");
} else {
return Err("expected command action".into());
}
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn test_bash_command_top_level_multiline_payload_executes() -> Result<()> {
let temp_dir = TempDir::new()?;
let bash_state_arc: Arc<Mutex<Option<BashState>>> = Arc::new(Mutex::new(None));
let init = Initialize {
init_type: InitializeType::FirstCall,
mode_name: ModeName::Wcgw,
any_workspace_path: temp_dir.path().to_string_lossy().to_string(),
thread_id: "dogfoodbash".to_string(),
code_writer_config: None,
initial_files_to_read: vec![],
task_id_to_resume: String::new(),
};
winx_code_agent::tools::initialize::handle_tool_call(&bash_state_arc, init).await?;
let command: BashCommand = serde_json::from_value(json!({
"thread_id": "dogfoodbash",
"wait_for_seconds": 2,
"command": "printf 'winx-one\\n'\nprintf 'winx-two\\n'"
}))
.map_err(|error| WinxError::ArgumentParseError(error.to_string()))?;
let response =
winx_code_agent::tools::bash_command::handle_tool_call(&bash_state_arc, command).await?;
assert!(response.contains("winx-one"), "{response}");
assert!(response.contains("winx-two"), "{response}");
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn test_read_files_single_file() -> Result<()> {
let temp_dir = TempDir::new()?;
let file_path = temp_dir.path().join("test.rs");
std::fs::write(&file_path, "fn main() {\n println!(\"Hello\");\n}\n")?;
let bash_state_arc: Arc<Mutex<Option<BashState>>> = Arc::new(Mutex::new(None));
let init = Initialize {
init_type: InitializeType::FirstCall,
mode_name: ModeName::Wcgw,
any_workspace_path: temp_dir.path().to_string_lossy().to_string(),
thread_id: String::new(),
code_writer_config: None,
initial_files_to_read: vec![],
task_id_to_resume: String::new(),
};
winx_code_agent::tools::initialize::handle_tool_call(&bash_state_arc, init).await?;
let read = ReadFiles {
file_paths: vec![file_path.to_string_lossy().to_string()],
thread_id: String::new(),
start_line_nums: vec![None],
end_line_nums: vec![None],
};
let response =
winx_code_agent::tools::read_files::handle_tool_call(&bash_state_arc, read).await?;
assert!(response.contains("fn main()"));
assert!(response.contains("println!"));
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn test_read_files_multiple_files() -> Result<()> {
let temp_dir = TempDir::new()?;
let file1 = temp_dir.path().join("file1.txt");
let file2 = temp_dir.path().join("file2.txt");
std::fs::write(&file1, "Content of file 1")?;
std::fs::write(&file2, "Content of file 2")?;
let bash_state_arc: Arc<Mutex<Option<BashState>>> = Arc::new(Mutex::new(None));
let init = Initialize {
init_type: InitializeType::FirstCall,
mode_name: ModeName::Wcgw,
any_workspace_path: temp_dir.path().to_string_lossy().to_string(),
thread_id: String::new(),
code_writer_config: None,
initial_files_to_read: vec![],
task_id_to_resume: String::new(),
};
winx_code_agent::tools::initialize::handle_tool_call(&bash_state_arc, init).await?;
let read = ReadFiles {
file_paths: vec![file1.to_string_lossy().to_string(), file2.to_string_lossy().to_string()],
thread_id: String::new(),
start_line_nums: vec![None, None],
end_line_nums: vec![None, None],
};
let response =
winx_code_agent::tools::read_files::handle_tool_call(&bash_state_arc, read).await?;
assert!(response.contains("Content of file 1"));
assert!(response.contains("Content of file 2"));
assert!(
response.find("Content of file 1") < response.find("Content of file 2"),
"parallel reads must preserve request order: {response}"
);
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn test_read_files_with_line_range() -> Result<()> {
let temp_dir = TempDir::new()?;
let file_path = temp_dir.path().join("lines.txt");
std::fs::write(&file_path, "Line 1\nLine 2\nLine 3\nLine 4\nLine 5\n")?;
let bash_state_arc: Arc<Mutex<Option<BashState>>> = Arc::new(Mutex::new(None));
let init = Initialize {
init_type: InitializeType::FirstCall,
mode_name: ModeName::Wcgw,
any_workspace_path: temp_dir.path().to_string_lossy().to_string(),
thread_id: String::new(),
code_writer_config: None,
initial_files_to_read: vec![],
task_id_to_resume: String::new(),
};
winx_code_agent::tools::initialize::handle_tool_call(&bash_state_arc, init).await?;
let read = ReadFiles {
file_paths: vec![file_path.to_string_lossy().to_string()],
thread_id: String::new(),
start_line_nums: vec![Some(2)],
end_line_nums: vec![Some(4)],
};
let response =
winx_code_agent::tools::read_files::handle_tool_call(&bash_state_arc, read).await?;
assert!(response.contains("Line 2"));
assert!(response.contains("Line 3"));
assert!(response.contains("Line 4"));
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn test_read_files_with_wcgw_path_suffix_range() -> Result<()> {
let temp_dir = TempDir::new()?;
let file_path = temp_dir.path().join("lines.txt");
std::fs::write(&file_path, "Line 1\nLine 2\nLine 3\nLine 4\nLine 5\n")?;
let bash_state_arc: Arc<Mutex<Option<BashState>>> = Arc::new(Mutex::new(None));
let init = Initialize {
init_type: InitializeType::FirstCall,
mode_name: ModeName::Wcgw,
any_workspace_path: temp_dir.path().to_string_lossy().to_string(),
thread_id: String::new(),
code_writer_config: None,
initial_files_to_read: vec![],
task_id_to_resume: String::new(),
};
winx_code_agent::tools::initialize::handle_tool_call(&bash_state_arc, init).await?;
let read: ReadFiles = serde_json::from_value(json!({
"file_paths": [format!("{}:2-4", file_path.to_string_lossy())]
}))
.map_err(|error| WinxError::ArgumentParseError(error.to_string()))?;
let response =
winx_code_agent::tools::read_files::handle_tool_call(&bash_state_arc, read).await?;
assert!(response.contains("2 Line 2"));
assert!(response.contains("3 Line 3"));
assert!(response.contains("4 Line 4"));
assert!(!response.contains("1 Line 1"));
assert!(!response.contains("5 Line 5"));
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn test_file_write_or_edit_treats_search_marker_as_edit_even_with_high_percentage(
) -> Result<()> {
let temp_dir = TempDir::new()?;
let file_path = temp_dir.path().join("edit.rs");
std::fs::write(&file_path, "fn main() {\n println!(\"old\");\n}\n")?;
let bash_state_arc: Arc<Mutex<Option<BashState>>> = Arc::new(Mutex::new(None));
let init = Initialize {
init_type: InitializeType::FirstCall,
mode_name: ModeName::Wcgw,
any_workspace_path: temp_dir.path().to_string_lossy().to_string(),
thread_id: "searchmarkeredit".to_string(),
code_writer_config: None,
initial_files_to_read: vec![],
task_id_to_resume: String::new(),
};
winx_code_agent::tools::initialize::handle_tool_call(&bash_state_arc, init).await?;
let read = ReadFiles {
file_paths: vec![file_path.to_string_lossy().to_string()],
thread_id: String::new(),
start_line_nums: vec![None],
end_line_nums: vec![None],
};
winx_code_agent::tools::read_files::handle_tool_call(&bash_state_arc, read).await?;
let edit = FileWriteOrEdit {
file_path: file_path.to_string_lossy().to_string(),
percentage_to_change: 100,
text_or_search_replace_blocks: r#"<<<<<<< SEARCH
println!("old");
=======
println!("new");
>>>>>>> REPLACE"#
.to_string(),
thread_id: "searchmarkeredit".to_string(),
};
winx_code_agent::tools::file_write_or_edit::handle_tool_call(&bash_state_arc, edit).await?;
let content = std::fs::read_to_string(&file_path)?;
assert!(content.contains("println!(\"new\")"));
assert!(!content.contains("<<<<<<< SEARCH"));
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn test_read_files_nonexistent() -> Result<()> {
let temp_dir = TempDir::new()?;
let bash_state_arc: Arc<Mutex<Option<BashState>>> = Arc::new(Mutex::new(None));
let init = Initialize {
init_type: InitializeType::FirstCall,
mode_name: ModeName::Wcgw,
any_workspace_path: temp_dir.path().to_string_lossy().to_string(),
thread_id: String::new(),
code_writer_config: None,
initial_files_to_read: vec![],
task_id_to_resume: String::new(),
};
winx_code_agent::tools::initialize::handle_tool_call(&bash_state_arc, init).await?;
let read = ReadFiles {
file_paths: vec![temp_dir.path().join("nonexistent.txt").to_string_lossy().to_string()],
thread_id: String::new(),
start_line_nums: vec![None],
end_line_nums: vec![None],
};
let result = winx_code_agent::tools::read_files::handle_tool_call(&bash_state_arc, read).await;
assert!(matches!(result, Err(WinxError::FileNotFound { .. })));
Ok(())
}
#[test]
fn test_bash_state_creation() {
let state = BashState::new();
assert!(!state.initialized);
let _ = &state.current_thread_id;
}
#[test]
fn test_bash_state_with_thread_id() {
let state = BashState::new_with_thread_id(Some("test-thread-123"));
let _ = state.current_thread_id;
}
#[test]
fn test_generate_thread_id() {
let id1 = winx_code_agent::state::bash_state::generate_thread_id();
let id2 = winx_code_agent::state::bash_state::generate_thread_id();
assert_ne!(id1, id2);
assert!(!id1.is_empty());
assert!(!id2.is_empty());
}
#[tokio::test]
async fn test_code_writer_mode() -> Result<()> {
use winx_code_agent::types::{AllowedCommands, AllowedGlobs, CodeWriterConfig};
let temp_dir = TempDir::new()?;
let workspace = temp_dir.path().canonicalize()?.to_string_lossy().to_string();
let bash_state_arc: Arc<Mutex<Option<BashState>>> = Arc::new(Mutex::new(None));
let init = Initialize {
init_type: InitializeType::FirstCall,
mode_name: ModeName::CodeWriter,
any_workspace_path: workspace.clone(),
thread_id: String::new(),
code_writer_config: Some(CodeWriterConfig {
allowed_globs: AllowedGlobs::List(vec!["*.rs".to_string(), "*.toml".to_string()]),
allowed_commands: AllowedCommands::List(vec!["cargo".to_string(), "rustc".to_string()]),
}),
initial_files_to_read: vec![],
task_id_to_resume: String::new(),
};
let response =
winx_code_agent::tools::initialize::handle_tool_call(&bash_state_arc, init).await?;
assert!(response.contains("CodeWriter") || response.contains("Initialized"));
let state = bash_state_arc.lock().await;
let bash_state = state.as_ref().ok_or(WinxError::BashStateNotInitialized)?;
assert!(bash_state.initialized);
assert_eq!(
bash_state.file_edit_mode.allowed_globs,
AllowedGlobs::List(vec![format!("{workspace}/*.rs"), format!("{workspace}/*.toml")])
);
assert_eq!(
bash_state.write_if_empty_mode.allowed_globs,
AllowedGlobs::List(vec![format!("{workspace}/*.rs"), format!("{workspace}/*.toml")])
);
assert_eq!(
bash_state.bash_command_mode.allowed_commands,
AllowedCommands::List(vec!["cargo".to_string(), "rustc".to_string()])
);
Ok(())
}
#[tokio::test]
async fn test_code_writer_mode_requires_config() -> Result<()> {
let temp_dir = TempDir::new()?;
let bash_state_arc: Arc<Mutex<Option<BashState>>> = Arc::new(Mutex::new(None));
let init = Initialize {
init_type: InitializeType::FirstCall,
mode_name: ModeName::CodeWriter,
any_workspace_path: temp_dir.path().to_string_lossy().to_string(),
thread_id: String::new(),
code_writer_config: None,
initial_files_to_read: vec![],
task_id_to_resume: String::new(),
};
let result = winx_code_agent::tools::initialize::handle_tool_call(&bash_state_arc, init).await;
assert!(matches!(result, Err(WinxError::ArgumentParseError(_))));
Ok(())
}
#[tokio::test]
async fn test_code_writer_mode_enforces_file_write_globs() -> Result<()> {
use winx_code_agent::types::{AllowedCommands, AllowedGlobs, CodeWriterConfig};
let temp_dir = TempDir::new()?;
let bash_state_arc: Arc<Mutex<Option<BashState>>> = Arc::new(Mutex::new(None));
let init = Initialize {
init_type: InitializeType::FirstCall,
mode_name: ModeName::CodeWriter,
any_workspace_path: temp_dir.path().to_string_lossy().to_string(),
thread_id: "codewritermode".to_string(),
code_writer_config: Some(CodeWriterConfig {
allowed_globs: AllowedGlobs::List(vec!["*.rs".to_string()]),
allowed_commands: AllowedCommands::List(vec![]),
}),
initial_files_to_read: vec![],
task_id_to_resume: String::new(),
};
winx_code_agent::tools::initialize::handle_tool_call(&bash_state_arc, init).await?;
let allowed_write = FileWriteOrEdit {
file_path: temp_dir.path().join("allowed.rs").to_string_lossy().to_string(),
percentage_to_change: 100,
text_or_search_replace_blocks: "fn main() {}\n".to_string(),
thread_id: "codewritermode".to_string(),
};
winx_code_agent::tools::file_write_or_edit::handle_tool_call(&bash_state_arc, allowed_write)
.await?;
let disallowed_write = FileWriteOrEdit {
file_path: temp_dir.path().join("blocked.txt").to_string_lossy().to_string(),
percentage_to_change: 100,
text_or_search_replace_blocks: "blocked\n".to_string(),
thread_id: "codewritermode".to_string(),
};
let result = winx_code_agent::tools::file_write_or_edit::handle_tool_call(
&bash_state_arc,
disallowed_write,
)
.await;
assert!(matches!(result, Err(WinxError::FileOperationDenied { .. })));
Ok(())
}
#[tokio::test]
async fn test_initialize_with_initial_files() -> Result<()> {
let temp_dir = TempDir::new()?;
let file_path = temp_dir.path().join("initial.rs");
std::fs::write(&file_path, "// Initial file content\nfn init() {}\n")?;
let bash_state_arc: Arc<Mutex<Option<BashState>>> = Arc::new(Mutex::new(None));
let init = Initialize {
init_type: InitializeType::FirstCall,
mode_name: ModeName::Wcgw,
any_workspace_path: temp_dir.path().to_string_lossy().to_string(),
thread_id: String::new(),
code_writer_config: None,
initial_files_to_read: vec![file_path.to_string_lossy().to_string()],
task_id_to_resume: String::new(),
};
let response =
winx_code_agent::tools::initialize::handle_tool_call(&bash_state_arc, init).await?;
assert!(response.contains("Initial file content") || response.contains("Requested files"));
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn test_context_save_basic() -> Result<()> {
use winx_code_agent::types::ContextSave;
let temp_dir = TempDir::new()?;
let test_file = temp_dir.path().join("src/main.rs");
std::fs::create_dir_all(
test_file
.parent()
.ok_or(WinxError::InvalidInput("missing parent directory".to_string()))?,
)?;
std::fs::write(&test_file, "fn main() { println!(\"Hello\"); }")?;
let bash_state_arc: Arc<Mutex<Option<BashState>>> = Arc::new(Mutex::new(None));
let init = Initialize {
init_type: InitializeType::FirstCall,
mode_name: ModeName::Wcgw,
any_workspace_path: temp_dir.path().to_string_lossy().to_string(),
thread_id: String::new(),
code_writer_config: None,
initial_files_to_read: vec![],
task_id_to_resume: String::new(),
};
winx_code_agent::tools::initialize::handle_tool_call(&bash_state_arc, init).await?;
let unique_id = format!(
"test-context-{}",
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map_or(0, |duration| duration.as_millis())
);
let context_save = ContextSave {
id: unique_id.clone(),
project_root_path: temp_dir.path().to_string_lossy().to_string(),
description: "Test context save for integration tests".to_string(),
relevant_file_globs: vec!["src/*.rs".to_string()],
thread_id: String::new(),
};
let response =
winx_code_agent::tools::context_save::handle_tool_call(&bash_state_arc, context_save)
.await?;
assert!(
response.contains(&unique_id) || response.contains(".txt") || response.contains("saved")
);
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn test_context_save_empty_globs() -> Result<()> {
use winx_code_agent::types::ContextSave;
let temp_dir = TempDir::new()?;
let bash_state_arc: Arc<Mutex<Option<BashState>>> = Arc::new(Mutex::new(None));
let init = Initialize {
init_type: InitializeType::FirstCall,
mode_name: ModeName::Wcgw,
any_workspace_path: temp_dir.path().to_string_lossy().to_string(),
thread_id: String::new(),
code_writer_config: None,
initial_files_to_read: vec![],
task_id_to_resume: String::new(),
};
winx_code_agent::tools::initialize::handle_tool_call(&bash_state_arc, init).await?;
let unique_id = format!(
"test-empty-{}",
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map_or(0, |duration| duration.as_millis())
);
let context_save = ContextSave {
id: unique_id,
project_root_path: temp_dir.path().to_string_lossy().to_string(),
description: "Test with empty globs".to_string(),
relevant_file_globs: vec![],
thread_id: String::new(),
};
let response =
winx_code_agent::tools::context_save::handle_tool_call(&bash_state_arc, context_save)
.await?;
assert!(response.contains(".txt") || response.contains("saved"));
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn test_context_save_no_matching_files() -> Result<()> {
use winx_code_agent::types::ContextSave;
let temp_dir = TempDir::new()?;
let bash_state_arc: Arc<Mutex<Option<BashState>>> = Arc::new(Mutex::new(None));
let init = Initialize {
init_type: InitializeType::FirstCall,
mode_name: ModeName::Wcgw,
any_workspace_path: temp_dir.path().to_string_lossy().to_string(),
thread_id: String::new(),
code_writer_config: None,
initial_files_to_read: vec![],
task_id_to_resume: String::new(),
};
winx_code_agent::tools::initialize::handle_tool_call(&bash_state_arc, init).await?;
let unique_id = format!(
"test-nomatch-{}",
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map_or(0, |duration| duration.as_millis())
);
let context_save = ContextSave {
id: unique_id,
project_root_path: temp_dir.path().to_string_lossy().to_string(),
description: "Test with non-matching glob".to_string(),
relevant_file_globs: vec!["*.nonexistent".to_string()],
thread_id: String::new(),
};
let response =
winx_code_agent::tools::context_save::handle_tool_call(&bash_state_arc, context_save)
.await?;
let response_lower = response.to_lowercase();
assert!(
response_lower.contains("no files")
|| response_lower.contains("warning")
|| response.contains(".txt")
);
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn test_read_image_png() -> Result<()> {
use winx_code_agent::types::ReadImage;
let temp_dir = TempDir::new()?;
let image_path = temp_dir.path().join("test.png");
let pixels = image::RgbaImage::from_pixel(1, 1, image::Rgba([255, 0, 0, 255]));
image::DynamicImage::ImageRgba8(pixels)
.save_with_format(&image_path, image::ImageFormat::Png)
.map_err(|error| WinxError::InvalidInput(error.to_string()))?;
let png_data = std::fs::read(&image_path)?;
let bash_state_arc: Arc<Mutex<Option<BashState>>> = Arc::new(Mutex::new(None));
let init = Initialize {
init_type: InitializeType::FirstCall,
mode_name: ModeName::Wcgw,
any_workspace_path: temp_dir.path().to_string_lossy().to_string(),
thread_id: String::new(),
code_writer_config: None,
initial_files_to_read: vec![],
task_id_to_resume: String::new(),
};
winx_code_agent::tools::initialize::handle_tool_call(&bash_state_arc, init).await?;
let read_image = ReadImage {
file_path: image_path.to_string_lossy().to_string(),
thread_id: String::new(),
force: false,
};
let (mime_type, base64_data) =
winx_code_agent::tools::read_image::handle_tool_call(&bash_state_arc, read_image).await?;
assert_eq!(mime_type, "image/png");
assert!(!base64_data.is_empty());
let decoded = general_purpose::STANDARD
.decode(&base64_data)
.map_err(|e| WinxError::InvalidInput(e.to_string()))?;
assert_eq!(decoded, png_data);
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn test_read_image_jpeg() -> Result<()> {
use winx_code_agent::types::ReadImage;
let temp_dir = TempDir::new()?;
let image_path = temp_dir.path().join("test.jpg");
let jpeg = image::RgbImage::from_pixel(1, 1, image::Rgb([220, 40, 80]));
image::DynamicImage::ImageRgb8(jpeg)
.save_with_format(&image_path, image::ImageFormat::Jpeg)
.map_err(|error| WinxError::InvalidInput(error.to_string()))?;
let bash_state_arc: Arc<Mutex<Option<BashState>>> = Arc::new(Mutex::new(None));
let init = Initialize {
init_type: InitializeType::FirstCall,
mode_name: ModeName::Wcgw,
any_workspace_path: temp_dir.path().to_string_lossy().to_string(),
thread_id: String::new(),
code_writer_config: None,
initial_files_to_read: vec![],
task_id_to_resume: String::new(),
};
winx_code_agent::tools::initialize::handle_tool_call(&bash_state_arc, init).await?;
let read_image = ReadImage {
file_path: image_path.to_string_lossy().to_string(),
thread_id: String::new(),
force: false,
};
let (mime_type, _base64_data) =
winx_code_agent::tools::read_image::handle_tool_call(&bash_state_arc, read_image).await?;
assert_eq!(mime_type, "image/jpeg");
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn test_read_image_nonexistent() -> Result<()> {
use winx_code_agent::types::ReadImage;
let temp_dir = TempDir::new()?;
let bash_state_arc: Arc<Mutex<Option<BashState>>> = Arc::new(Mutex::new(None));
let init = Initialize {
init_type: InitializeType::FirstCall,
mode_name: ModeName::Wcgw,
any_workspace_path: temp_dir.path().to_string_lossy().to_string(),
thread_id: String::new(),
code_writer_config: None,
initial_files_to_read: vec![],
task_id_to_resume: String::new(),
};
winx_code_agent::tools::initialize::handle_tool_call(&bash_state_arc, init).await?;
let read_image = ReadImage {
file_path: temp_dir.path().join("nonexistent.png").to_string_lossy().to_string(),
thread_id: String::new(),
force: false,
};
let result =
winx_code_agent::tools::read_image::handle_tool_call(&bash_state_arc, read_image).await;
assert!(result.is_err());
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn test_read_image_non_image_file() -> Result<()> {
use winx_code_agent::types::ReadImage;
let temp_dir = TempDir::new()?;
let text_path = temp_dir.path().join("test.txt");
std::fs::write(&text_path, "This is not an image")?;
let bash_state_arc: Arc<Mutex<Option<BashState>>> = Arc::new(Mutex::new(None));
let init = Initialize {
init_type: InitializeType::FirstCall,
mode_name: ModeName::Wcgw,
any_workspace_path: temp_dir.path().to_string_lossy().to_string(),
thread_id: String::new(),
code_writer_config: None,
initial_files_to_read: vec![],
task_id_to_resume: String::new(),
};
winx_code_agent::tools::initialize::handle_tool_call(&bash_state_arc, init).await?;
let read_image = ReadImage {
file_path: text_path.to_string_lossy().to_string(),
thread_id: String::new(),
force: false,
};
let result =
winx_code_agent::tools::read_image::handle_tool_call(&bash_state_arc, read_image).await;
if let Ok((mime_type, base64_data)) = result {
assert!(
mime_type == "image/jpeg"
|| mime_type == "text/plain"
|| mime_type.starts_with("text/")
);
assert!(!base64_data.is_empty());
} else {
}
Ok(())
}