Test framework improvements and fixture updates
This commit is contained in:
parent
b38574c588
commit
1232b2fc65
24 changed files with 208 additions and 266 deletions
|
|
@ -131,7 +131,7 @@ impl ConversationTest {
|
|||
ConversationBuilder::new(bot_name).build()
|
||||
}
|
||||
|
||||
pub async fn with_context(ctx: &TestContext, bot_name: &str) -> Result<Self> {
|
||||
pub fn with_context(ctx: &TestContext, bot_name: &str) -> Result<Self> {
|
||||
let mut conv = ConversationBuilder::new(bot_name).build();
|
||||
conv.llm_url = Some(ctx.llm_url());
|
||||
Ok(conv)
|
||||
|
|
@ -306,7 +306,7 @@ impl ConversationTest {
|
|||
metadata
|
||||
}
|
||||
|
||||
pub async fn assert_response_contains(&mut self, text: &str) -> &mut Self {
|
||||
pub fn assert_response_contains(&mut self, text: &str) -> &mut Self {
|
||||
let result = if let Some(ref response) = self.last_response {
|
||||
if response.content.contains(text) {
|
||||
AssertionResult::pass(&format!("Response contains '{text}'"))
|
||||
|
|
@ -325,7 +325,7 @@ impl ConversationTest {
|
|||
self
|
||||
}
|
||||
|
||||
pub async fn assert_response_equals(&mut self, text: &str) -> &mut Self {
|
||||
pub fn assert_response_equals(&mut self, text: &str) -> &mut Self {
|
||||
let result = if let Some(ref response) = self.last_response {
|
||||
if response.content == text {
|
||||
AssertionResult::pass(&format!("Response equals '{text}'"))
|
||||
|
|
@ -344,7 +344,7 @@ impl ConversationTest {
|
|||
self
|
||||
}
|
||||
|
||||
pub async fn assert_response_matches(&mut self, pattern: &str) -> &mut Self {
|
||||
pub fn assert_response_matches(&mut self, pattern: &str) -> &mut Self {
|
||||
let result = if let Some(ref response) = self.last_response {
|
||||
match regex::Regex::new(pattern) {
|
||||
Ok(re) => {
|
||||
|
|
@ -372,7 +372,7 @@ impl ConversationTest {
|
|||
self
|
||||
}
|
||||
|
||||
pub async fn assert_response_not_contains(&mut self, text: &str) -> &mut Self {
|
||||
pub fn assert_response_not_contains(&mut self, text: &str) -> &mut Self {
|
||||
let result = if let Some(ref response) = self.last_response {
|
||||
if response.content.contains(text) {
|
||||
AssertionResult::fail(
|
||||
|
|
@ -391,16 +391,13 @@ impl ConversationTest {
|
|||
self
|
||||
}
|
||||
|
||||
pub async fn assert_transferred_to_human(&mut self) -> &mut Self {
|
||||
pub fn assert_transferred_to_human(&mut self) -> &mut Self {
|
||||
let is_transferred = self.state == ConversationState::Transferred
|
||||
|| self
|
||||
.last_response
|
||||
.as_ref()
|
||||
.is_some_and(|r| {
|
||||
r.content.to_lowercase().contains("transfer")
|
||||
|| r.content.to_lowercase().contains("human")
|
||||
|| r.content.to_lowercase().contains("agent")
|
||||
});
|
||||
|| self.last_response.as_ref().is_some_and(|r| {
|
||||
r.content.to_lowercase().contains("transfer")
|
||||
|| r.content.to_lowercase().contains("human")
|
||||
|| r.content.to_lowercase().contains("agent")
|
||||
});
|
||||
|
||||
let result = if is_transferred {
|
||||
self.state = ConversationState::Transferred;
|
||||
|
|
@ -417,7 +414,7 @@ impl ConversationTest {
|
|||
self
|
||||
}
|
||||
|
||||
pub async fn assert_queue_position(&mut self, expected: usize) -> &mut Self {
|
||||
pub fn assert_queue_position(&mut self, expected: usize) -> &mut Self {
|
||||
let actual = self
|
||||
.context
|
||||
.get("queue_position")
|
||||
|
|
@ -438,7 +435,7 @@ impl ConversationTest {
|
|||
self
|
||||
}
|
||||
|
||||
pub async fn assert_response_within(&mut self, max_duration: Duration) -> &mut Self {
|
||||
pub fn assert_response_within(&mut self, max_duration: Duration) -> &mut Self {
|
||||
let result = if let Some(latency) = self.last_latency {
|
||||
if latency <= max_duration {
|
||||
AssertionResult::pass(&format!("Response within {max_duration:?}"))
|
||||
|
|
@ -461,7 +458,7 @@ impl ConversationTest {
|
|||
self
|
||||
}
|
||||
|
||||
pub async fn assert_response_count(&mut self, expected: usize) -> &mut Self {
|
||||
pub fn assert_response_count(&mut self, expected: usize) -> &mut Self {
|
||||
let actual = self.responses.len();
|
||||
|
||||
let result = if actual == expected {
|
||||
|
|
@ -478,7 +475,7 @@ impl ConversationTest {
|
|||
self
|
||||
}
|
||||
|
||||
pub async fn assert_response_type(&mut self, expected: ResponseContentType) -> &mut Self {
|
||||
pub fn assert_response_type(&mut self, expected: ResponseContentType) -> &mut Self {
|
||||
let result = if let Some(ref response) = self.last_response {
|
||||
if response.content_type == expected {
|
||||
AssertionResult::pass(&format!("Response type is {expected:?}"))
|
||||
|
|
@ -510,7 +507,7 @@ impl ConversationTest {
|
|||
self.context.get(key)
|
||||
}
|
||||
|
||||
pub async fn end(&mut self) -> &mut Self {
|
||||
pub fn end(&mut self) -> &mut Self {
|
||||
self.state = ConversationState::Ended;
|
||||
self.record.ended_at = Some(Utc::now());
|
||||
self
|
||||
|
|
@ -583,7 +580,7 @@ mod tests {
|
|||
async fn test_assert_response_contains() {
|
||||
let mut conv = ConversationTest::new("test-bot");
|
||||
conv.user_says("test").await;
|
||||
conv.assert_response_contains("Response").await;
|
||||
conv.assert_response_contains("Response");
|
||||
|
||||
assert!(conv.all_passed());
|
||||
}
|
||||
|
|
@ -592,7 +589,7 @@ mod tests {
|
|||
async fn test_assert_response_not_contains() {
|
||||
let mut conv = ConversationTest::new("test-bot");
|
||||
conv.user_says("test").await;
|
||||
conv.assert_response_not_contains("nonexistent").await;
|
||||
conv.assert_response_not_contains("nonexistent");
|
||||
|
||||
assert!(conv.all_passed());
|
||||
}
|
||||
|
|
@ -632,7 +629,7 @@ mod tests {
|
|||
async fn test_end_conversation() {
|
||||
let mut conv = ConversationTest::new("test-bot");
|
||||
conv.user_says("bye").await;
|
||||
conv.end().await;
|
||||
conv.end();
|
||||
|
||||
assert_eq!(conv.state(), ConversationState::Ended);
|
||||
assert!(conv.record().ended_at.is_some());
|
||||
|
|
@ -642,7 +639,7 @@ mod tests {
|
|||
async fn test_failed_assertions() {
|
||||
let mut conv = ConversationTest::new("test-bot");
|
||||
conv.user_says("test").await;
|
||||
conv.assert_response_equals("this will not match").await;
|
||||
conv.assert_response_equals("this will not match");
|
||||
|
||||
assert!(!conv.all_passed());
|
||||
assert_eq!(conv.failed_assertions().len(), 1);
|
||||
|
|
@ -672,13 +669,13 @@ mod tests {
|
|||
let mut conv = ConversationTest::new("support-bot");
|
||||
|
||||
conv.user_says("Hi").await;
|
||||
conv.assert_response_contains("Response").await;
|
||||
conv.assert_response_contains("Response");
|
||||
|
||||
conv.user_says("I need help").await;
|
||||
conv.assert_response_contains("Response").await;
|
||||
conv.assert_response_contains("Response");
|
||||
|
||||
conv.user_says("Thanks, bye").await;
|
||||
conv.end().await;
|
||||
conv.end();
|
||||
|
||||
assert_eq!(conv.sent_messages().len(), 3);
|
||||
assert_eq!(conv.responses().len(), 3);
|
||||
|
|
@ -689,7 +686,7 @@ mod tests {
|
|||
async fn test_response_time_assertion() {
|
||||
let mut conv = ConversationTest::new("test-bot");
|
||||
conv.user_says("quick test").await;
|
||||
conv.assert_response_within(Duration::from_secs(5)).await;
|
||||
conv.assert_response_within(Duration::from_secs(5));
|
||||
|
||||
assert!(conv.all_passed());
|
||||
}
|
||||
|
|
@ -699,7 +696,7 @@ mod tests {
|
|||
let mut conv = ConversationTest::new("test-bot");
|
||||
conv.user_says("one").await;
|
||||
conv.user_says("two").await;
|
||||
conv.assert_response_count(2).await;
|
||||
conv.assert_response_count(2);
|
||||
|
||||
assert!(conv.all_passed());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -254,6 +254,7 @@ pub struct Screenshot {
|
|||
|
||||
impl Screenshot {
|
||||
pub fn save(&self, path: impl Into<PathBuf>) -> Result<()> {
|
||||
let _ = (&self.data, self.width, self.height);
|
||||
let path = path.into();
|
||||
anyhow::bail!("Screenshot save not yet implemented: {}", path.display())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::{json, Value};
|
||||
use std::collections::HashMap;
|
||||
|
|
@ -334,6 +333,7 @@ pub fn sample_faqs() -> Vec<FAQ> {
|
|||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[allow(clippy::upper_case_acronyms)]
|
||||
pub struct FAQ {
|
||||
pub id: u32,
|
||||
pub question: String,
|
||||
|
|
@ -425,10 +425,12 @@ mod tests {
|
|||
fn test_whatsapp_text_message() {
|
||||
let payload = whatsapp_text_message("15551234567", "Hello");
|
||||
assert_eq!(payload["object"], "whatsapp_business_account");
|
||||
assert!(payload["entry"][0]["changes"][0]["value"]["messages"][0]["text"]["body"]
|
||||
.as_str()
|
||||
.unwrap()
|
||||
.contains("Hello"));
|
||||
assert!(
|
||||
payload["entry"][0]["changes"][0]["value"]["messages"][0]["text"]["body"]
|
||||
.as_str()
|
||||
.unwrap()
|
||||
.contains("Hello")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -453,7 +455,9 @@ mod tests {
|
|||
fn test_sample_kb_entries() {
|
||||
let entries = sample_kb_entries();
|
||||
assert!(!entries.is_empty());
|
||||
assert!(entries.iter().any(|e| e.category == Some("products".to_string())));
|
||||
assert!(entries
|
||||
.iter()
|
||||
.any(|e| e.category == Some("products".to_string())));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
|
||||
pub mod data;
|
||||
pub mod scripts;
|
||||
|
||||
|
|
@ -7,7 +6,6 @@ use serde::{Deserialize, Serialize};
|
|||
use std::collections::HashMap;
|
||||
use uuid::Uuid;
|
||||
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct User {
|
||||
pub id: Uuid,
|
||||
|
|
@ -79,6 +77,7 @@ impl Default for Customer {
|
|||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
#[allow(clippy::upper_case_acronyms)]
|
||||
pub enum Channel {
|
||||
WhatsApp,
|
||||
Teams,
|
||||
|
|
@ -278,7 +277,6 @@ impl Default for QueueStatus {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
#[must_use]
|
||||
pub fn admin_user() -> User {
|
||||
User {
|
||||
|
|
|
|||
|
|
@ -493,14 +493,18 @@ impl BotServerInstance {
|
|||
|
||||
if !stack_path.exists() {
|
||||
anyhow::bail!(
|
||||
"Main botserver-stack not found at {stack_path:?}.\n\
|
||||
Run botserver once to initialize: cd ../botserver && cargo run"
|
||||
"Main botserver-stack not found at {}.\n\
|
||||
Run botserver once to initialize: cd ../botserver && cargo run",
|
||||
stack_path.display()
|
||||
);
|
||||
}
|
||||
|
||||
log::info!("Starting botserver with MAIN stack at {stack_path:?}");
|
||||
log::info!(
|
||||
"Starting botserver with MAIN stack at {}",
|
||||
stack_path.display()
|
||||
);
|
||||
println!("🚀 Starting BotServer with main stack...");
|
||||
println!(" Stack: {stack_path:?}");
|
||||
println!(" Stack: {}", stack_path.display());
|
||||
|
||||
let process = std::process::Command::new(&botserver_bin_path)
|
||||
.current_dir(&botserver_dir)
|
||||
|
|
@ -606,7 +610,7 @@ impl BotUIInstance {
|
|||
log::info!("Starting botui from: {botui_bin} on port {port}");
|
||||
log::info!(" BOTUI_PORT={port}");
|
||||
log::info!(" BOTSERVER_URL={botserver_url}");
|
||||
log::info!(" Working directory: {botui_dir:?}");
|
||||
log::info!(" Working directory: {}", botui_dir.display());
|
||||
|
||||
let process = std::process::Command::new(&botui_bin_path)
|
||||
.current_dir(&botui_dir)
|
||||
|
|
@ -672,7 +676,7 @@ impl BotServerInstance {
|
|||
let stack_path = ctx.data_dir.join("botserver-stack");
|
||||
std::fs::create_dir_all(&stack_path)?;
|
||||
let stack_path = stack_path.canonicalize().unwrap_or(stack_path);
|
||||
log::info!("Created clean test stack at: {stack_path:?}");
|
||||
log::info!("Created clean test stack at: {}", stack_path.display());
|
||||
|
||||
let botserver_bin = std::env::var("BOTSERVER_BIN")
|
||||
.unwrap_or_else(|_| "../botserver/target/debug/botserver".to_string());
|
||||
|
|
@ -701,12 +705,12 @@ impl BotServerInstance {
|
|||
.unwrap_or_else(|_| PathBuf::from("../botserver"))
|
||||
});
|
||||
|
||||
log::info!("Botserver working directory: {botserver_dir:?}");
|
||||
log::info!("Stack path (absolute): {stack_path:?}");
|
||||
log::info!("Botserver working directory: {}", botserver_dir.display());
|
||||
log::info!("Stack path (absolute): {}", stack_path.display());
|
||||
|
||||
let installers_path = botserver_dir.join("botserver-installers");
|
||||
let installers_path = installers_path.canonicalize().unwrap_or(installers_path);
|
||||
log::info!("Using installers from: {installers_path:?}");
|
||||
log::info!("Using installers from: {}", installers_path.display());
|
||||
|
||||
let process = std::process::Command::new(&botserver_bin_path)
|
||||
.current_dir(&botserver_dir)
|
||||
|
|
@ -764,7 +768,7 @@ impl BotServerInstance {
|
|||
self.process.is_some()
|
||||
}
|
||||
|
||||
fn setup_test_stack_config(stack_path: &PathBuf, ctx: &TestContext) -> Result<()> {
|
||||
fn setup_test_stack_config(stack_path: &std::path::Path, ctx: &TestContext) -> Result<()> {
|
||||
let directory_conf = stack_path.join("conf/directory");
|
||||
std::fs::create_dir_all(&directory_conf)?;
|
||||
|
||||
|
|
@ -800,7 +804,7 @@ ExternalPort: {}
|
|||
Ok(())
|
||||
}
|
||||
|
||||
fn generate_test_certificates(certs_dir: &PathBuf) -> Result<()> {
|
||||
fn generate_test_certificates(certs_dir: &std::path::Path) -> Result<()> {
|
||||
use std::process::Command;
|
||||
|
||||
let api_dir = certs_dir.join("api");
|
||||
|
|
@ -922,7 +926,8 @@ impl TestHarness {
|
|||
};
|
||||
|
||||
log::info!(
|
||||
"Test {test_id} allocated ports: {ports:?}, data_dir: {data_dir:?}, use_existing_stack: {use_existing_stack}"
|
||||
"Test {test_id} allocated ports: {ports:?}, data_dir: {}, use_existing_stack: {use_existing_stack}",
|
||||
data_dir.display()
|
||||
);
|
||||
|
||||
let data_dir_str = data_dir.to_str().unwrap().to_string();
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ pub mod prelude {
|
|||
mod tests {
|
||||
#[test]
|
||||
fn test_library_loads() {
|
||||
assert!(true);
|
||||
let version = env!("CARGO_PKG_VERSION");
|
||||
assert!(!version.is_empty());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
23
src/main.rs
23
src/main.rs
|
|
@ -308,7 +308,7 @@ async fn download_file(url: &str, dest: &PathBuf) -> Result<()> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
async fn extract_zip(zip_path: &PathBuf, dest_dir: &PathBuf) -> Result<()> {
|
||||
fn extract_zip(zip_path: &PathBuf, dest_dir: &PathBuf) -> Result<()> {
|
||||
info!("Extracting: {:?} to {:?}", zip_path, dest_dir);
|
||||
|
||||
let file = std::fs::File::open(zip_path)?;
|
||||
|
|
@ -390,7 +390,7 @@ async fn setup_chromedriver(browser_path: &str) -> Result<PathBuf> {
|
|||
let zip_path = cache_dir.join("chromedriver.zip");
|
||||
download_file(&chromedriver_url, &zip_path).await?;
|
||||
|
||||
extract_zip(&zip_path, &cache_dir).await?;
|
||||
extract_zip(&zip_path, &cache_dir)?;
|
||||
|
||||
let extracted_driver = cache_dir.join("chromedriver-linux64").join("chromedriver");
|
||||
let final_path = get_chromedriver_path(&major_version);
|
||||
|
|
@ -439,7 +439,7 @@ async fn setup_chrome_for_testing() -> Result<PathBuf> {
|
|||
let zip_path = cache_dir.join("chrome.zip");
|
||||
download_file(&chrome_url, &zip_path).await?;
|
||||
|
||||
extract_zip(&zip_path, &cache_dir).await?;
|
||||
extract_zip(&zip_path, &cache_dir)?;
|
||||
|
||||
std::fs::remove_file(&zip_path).ok();
|
||||
|
||||
|
|
@ -494,12 +494,11 @@ async fn start_chromedriver(chromedriver_path: &PathBuf, port: u16) -> Result<st
|
|||
async fn check_webdriver_available(port: u16) -> bool {
|
||||
let url = format!("http://localhost:{port}/status");
|
||||
|
||||
let client = match reqwest::Client::builder()
|
||||
let Ok(client) = reqwest::Client::builder()
|
||||
.timeout(std::time::Duration::from_secs(2))
|
||||
.build()
|
||||
{
|
||||
Ok(c) => c,
|
||||
Err(_) => return false,
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
|
||||
client.get(&url).send().await.is_ok()
|
||||
|
|
@ -641,7 +640,7 @@ fn run_cargo_test(
|
|||
Ok((passed, failed, skipped))
|
||||
}
|
||||
|
||||
async fn run_unit_tests(config: &RunnerConfig) -> Result<TestResults> {
|
||||
fn run_unit_tests(config: &RunnerConfig) -> Result<TestResults> {
|
||||
info!("Running unit tests...");
|
||||
|
||||
let mut results = TestResults::new("unit");
|
||||
|
|
@ -998,8 +997,8 @@ async fn main() -> ExitCode {
|
|||
match setup_test_dependencies().await {
|
||||
Ok((chromedriver, chrome)) => {
|
||||
println!("\n✅ Dependencies installed successfully!");
|
||||
println!(" ChromeDriver: {chromedriver:?}");
|
||||
println!(" Browser: {chrome:?}");
|
||||
println!(" ChromeDriver: {}", chromedriver.display());
|
||||
println!(" Browser: {}", chrome.display());
|
||||
return ExitCode::SUCCESS;
|
||||
}
|
||||
Err(e) => {
|
||||
|
|
@ -1029,11 +1028,11 @@ async fn main() -> ExitCode {
|
|||
let mut all_results = Vec::new();
|
||||
|
||||
let result = match config.suite {
|
||||
TestSuite::Unit => run_unit_tests(&config).await,
|
||||
TestSuite::Unit => run_unit_tests(&config),
|
||||
TestSuite::Integration => run_integration_tests(&config).await,
|
||||
TestSuite::E2E => run_e2e_tests(&config).await,
|
||||
TestSuite::All => {
|
||||
let unit = run_unit_tests(&config).await;
|
||||
let unit = run_unit_tests(&config);
|
||||
let integration = run_integration_tests(&config).await;
|
||||
let e2e = run_e2e_tests(&config).await;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
use super::{new_expectation_store, Expectation, ExpectationStore};
|
||||
use anyhow::{Context, Result};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt::Write;
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::time::Duration;
|
||||
|
|
@ -89,6 +90,7 @@ struct ChatChoice {
|
|||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[allow(clippy::struct_field_names)]
|
||||
struct Usage {
|
||||
prompt_tokens: u32,
|
||||
completion_tokens: u32,
|
||||
|
|
@ -219,11 +221,13 @@ impl MockLLM {
|
|||
.unwrap()
|
||||
.push(expectation.clone());
|
||||
|
||||
let mut store = self.expectations.lock().unwrap();
|
||||
store.insert(
|
||||
format!("completion:{prompt_contains}"),
|
||||
Expectation::new(&format!("completion containing '{prompt_contains}'")),
|
||||
);
|
||||
{
|
||||
let mut store = self.expectations.lock().unwrap();
|
||||
store.insert(
|
||||
format!("completion:{prompt_contains}"),
|
||||
Expectation::new(&format!("completion containing '{prompt_contains}'")),
|
||||
);
|
||||
}
|
||||
|
||||
let response_text = response.to_string();
|
||||
let model = self.default_model.clone();
|
||||
|
|
@ -253,7 +257,8 @@ impl MockLLM {
|
|||
|
||||
let mut template = ResponseTemplate::new(200).set_body_json(&response_body);
|
||||
|
||||
if let Some(delay) = *latency.lock().unwrap() {
|
||||
let latency_value = *latency.lock().unwrap();
|
||||
if let Some(delay) = latency_value {
|
||||
template = template.set_delay(delay);
|
||||
}
|
||||
|
||||
|
|
@ -303,10 +308,11 @@ impl MockLLM {
|
|||
finish_reason: None,
|
||||
}],
|
||||
};
|
||||
sse_body.push_str(&format!(
|
||||
"data: {}\n\n",
|
||||
let _ = writeln!(
|
||||
sse_body,
|
||||
"data: {}\n",
|
||||
serde_json::to_string(&first_chunk).unwrap()
|
||||
));
|
||||
);
|
||||
|
||||
for chunk_text in &chunks {
|
||||
let chunk = StreamChunk {
|
||||
|
|
@ -323,10 +329,11 @@ impl MockLLM {
|
|||
finish_reason: None,
|
||||
}],
|
||||
};
|
||||
sse_body.push_str(&format!(
|
||||
"data: {}\n\n",
|
||||
let _ = writeln!(
|
||||
sse_body,
|
||||
"data: {}\n",
|
||||
serde_json::to_string(&chunk).unwrap()
|
||||
));
|
||||
);
|
||||
}
|
||||
|
||||
let final_chunk = StreamChunk {
|
||||
|
|
@ -343,10 +350,11 @@ impl MockLLM {
|
|||
finish_reason: Some("stop".to_string()),
|
||||
}],
|
||||
};
|
||||
sse_body.push_str(&format!(
|
||||
"data: {}\n\n",
|
||||
let _ = writeln!(
|
||||
sse_body,
|
||||
"data: {}\n",
|
||||
serde_json::to_string(&final_chunk).unwrap()
|
||||
));
|
||||
);
|
||||
sse_body.push_str("data: [DONE]\n\n");
|
||||
|
||||
let template = ResponseTemplate::new(200)
|
||||
|
|
@ -596,10 +604,7 @@ impl MockLLM {
|
|||
}
|
||||
|
||||
pub async fn call_count(&self) -> usize {
|
||||
self.server
|
||||
.received_requests()
|
||||
.await
|
||||
.map_or(0, |r| r.len())
|
||||
self.server.received_requests().await.map_or(0, |r| r.len())
|
||||
}
|
||||
|
||||
pub async fn assert_called_times(&self, expected: usize) {
|
||||
|
|
@ -649,7 +654,7 @@ mod tests {
|
|||
let response = ChatCompletionResponse {
|
||||
id: "test-id".to_string(),
|
||||
object: "chat.completion".to_string(),
|
||||
created: 1234567890,
|
||||
created: 1_234_567_890,
|
||||
model: "gpt-4".to_string(),
|
||||
choices: vec![ChatChoice {
|
||||
index: 0,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
|
||||
use super::{new_expectation_store, ExpectationStore};
|
||||
use anyhow::{Context, Result};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
|
@ -22,6 +21,7 @@ pub struct MockTeams {
|
|||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[allow(clippy::struct_field_names)]
|
||||
pub struct Activity {
|
||||
#[serde(rename = "type")]
|
||||
pub activity_type: String,
|
||||
|
|
@ -124,6 +124,7 @@ pub struct Attachment {
|
|||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[allow(clippy::struct_field_names)]
|
||||
pub struct Entity {
|
||||
#[serde(rename = "type")]
|
||||
pub entity_type: String,
|
||||
|
|
@ -318,9 +319,7 @@ impl MockTeams {
|
|||
|
||||
sent_activities.lock().unwrap().push(activity.clone());
|
||||
|
||||
let response = ResourceResponse {
|
||||
id: activity.id,
|
||||
};
|
||||
let response = ResourceResponse { id: activity.id };
|
||||
|
||||
ResponseTemplate::new(200).set_body_json(&response)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
|
||||
use super::{new_expectation_store, ExpectationStore};
|
||||
use anyhow::{Context, Result};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
|
@ -213,7 +212,8 @@ pub struct ConversationOrigin {
|
|||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Pricing {
|
||||
pub billable: bool,
|
||||
pub pricing_model: String,
|
||||
#[serde(alias = "pricing_model")]
|
||||
pub model: String,
|
||||
pub category: String,
|
||||
}
|
||||
|
||||
|
|
@ -354,7 +354,6 @@ impl MockWhatsApp {
|
|||
|
||||
async fn setup_default_routes(&self) {
|
||||
let sent_messages = self.sent_messages.clone();
|
||||
let _phone_id = self.phone_number_id.clone();
|
||||
|
||||
Mock::given(method("POST"))
|
||||
.and(path_regex(r"/v\d+\.\d+/\d+/messages"))
|
||||
|
|
@ -413,7 +412,6 @@ impl MockWhatsApp {
|
|||
id: message_id.clone(),
|
||||
to: to.to_string(),
|
||||
message_type: match msg_type {
|
||||
"text" => MessageType::Text,
|
||||
"template" => MessageType::Template,
|
||||
"image" => MessageType::Image,
|
||||
"document" => MessageType::Document,
|
||||
|
|
@ -483,6 +481,7 @@ impl MockWhatsApp {
|
|||
|
||||
#[must_use]
|
||||
pub fn expect_send_message(&self, to: &str) -> MessageExpectation {
|
||||
let _ = self;
|
||||
MessageExpectation {
|
||||
to: to.to_string(),
|
||||
message_type: None,
|
||||
|
|
@ -492,6 +491,7 @@ impl MockWhatsApp {
|
|||
|
||||
#[must_use]
|
||||
pub fn expect_send_template(&self, name: &str) -> TemplateExpectation {
|
||||
let _ = self;
|
||||
TemplateExpectation {
|
||||
name: name.to_string(),
|
||||
to: None,
|
||||
|
|
@ -707,7 +707,7 @@ impl MockWhatsApp {
|
|||
}),
|
||||
pricing: Some(Pricing {
|
||||
billable: true,
|
||||
pricing_model: "CBP".to_string(),
|
||||
model: "CBP".to_string(),
|
||||
category: "business_initiated".to_string(),
|
||||
}),
|
||||
}]),
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
|
||||
use super::{new_expectation_store, ExpectationStore};
|
||||
use anyhow::{Context, Result};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
|
@ -405,10 +404,7 @@ impl MockZitadel {
|
|||
|
||||
Mock::given(method("GET"))
|
||||
.and(path("/oidc/v1/userinfo"))
|
||||
.and(header(
|
||||
"authorization",
|
||||
format!("Bearer {token}").as_str(),
|
||||
))
|
||||
.and(header("authorization", format!("Bearer {token}").as_str()))
|
||||
.respond_with(ResponseTemplate::new(200).set_body_json(&response))
|
||||
.mount(&self.server)
|
||||
.await;
|
||||
|
|
@ -663,8 +659,8 @@ mod tests {
|
|||
client_id: Some("client".to_string()),
|
||||
username: Some("user@test.com".to_string()),
|
||||
token_type: Some("Bearer".to_string()),
|
||||
exp: Some(1234567890),
|
||||
iat: Some(1234567800),
|
||||
exp: Some(1_234_567_890),
|
||||
iat: Some(1_234_567_800),
|
||||
sub: Some("user-id".to_string()),
|
||||
aud: Some("audience".to_string()),
|
||||
iss: Some("issuer".to_string()),
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
|
||||
use anyhow::{Context, Result};
|
||||
use log::{info, warn};
|
||||
use std::process::{Child, Command, Stdio};
|
||||
|
|
@ -169,6 +168,7 @@ impl BrowserService {
|
|||
self.port
|
||||
}
|
||||
|
||||
#[allow(clippy::unused_async)]
|
||||
pub async fn stop(&mut self) -> Result<()> {
|
||||
if let Some(mut process) = self.process.take() {
|
||||
info!("Stopping browser");
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
|
||||
use super::{check_tcp_port, ensure_dir, wait_for, HEALTH_CHECK_INTERVAL, HEALTH_CHECK_TIMEOUT};
|
||||
use anyhow::{Context, Result};
|
||||
use nix::sys::signal::{kill, Signal};
|
||||
|
|
@ -28,7 +27,10 @@ impl MinioService {
|
|||
if let Ok(stack_path) = std::env::var("BOTSERVER_STACK_PATH") {
|
||||
let minio_path = PathBuf::from(&stack_path).join("bin/drive/minio");
|
||||
if minio_path.exists() {
|
||||
log::info!("Using MinIO from BOTSERVER_STACK_PATH: {minio_path:?}");
|
||||
log::info!(
|
||||
"Using MinIO from BOTSERVER_STACK_PATH: {}",
|
||||
minio_path.display()
|
||||
);
|
||||
return Ok(minio_path);
|
||||
}
|
||||
}
|
||||
|
|
@ -43,7 +45,7 @@ impl MinioService {
|
|||
for rel_path in &relative_paths {
|
||||
let minio_path = cwd.join(rel_path);
|
||||
if minio_path.exists() {
|
||||
log::info!("Using MinIO from botserver-stack: {minio_path:?}");
|
||||
log::info!("Using MinIO from botserver-stack: {}", minio_path.display());
|
||||
return Ok(minio_path);
|
||||
}
|
||||
}
|
||||
|
|
@ -58,13 +60,13 @@ impl MinioService {
|
|||
for path in &system_paths {
|
||||
let minio_path = PathBuf::from(path);
|
||||
if minio_path.exists() {
|
||||
log::info!("Using system MinIO from: {minio_path:?}");
|
||||
log::info!("Using system MinIO from: {}", minio_path.display());
|
||||
return Ok(minio_path);
|
||||
}
|
||||
}
|
||||
|
||||
if let Ok(minio_path) = which::which("minio") {
|
||||
log::info!("Using MinIO from PATH: {minio_path:?}");
|
||||
log::info!("Using MinIO from PATH: {}", minio_path.display());
|
||||
return Ok(minio_path);
|
||||
}
|
||||
|
||||
|
|
@ -73,7 +75,7 @@ impl MinioService {
|
|||
|
||||
pub async fn start(api_port: u16, data_dir: &str) -> Result<Self> {
|
||||
let bin_path = Self::find_minio_binary()?;
|
||||
log::info!("Using MinIO from: {bin_path:?}");
|
||||
log::info!("Using MinIO from: {}", bin_path.display());
|
||||
|
||||
let data_path = PathBuf::from(data_dir).join("minio");
|
||||
ensure_dir(&data_path)?;
|
||||
|
|
@ -90,7 +92,7 @@ impl MinioService {
|
|||
secret_key: Self::DEFAULT_SECRET_KEY.to_string(),
|
||||
};
|
||||
|
||||
service.start_server().await?;
|
||||
service.start_server()?;
|
||||
service.wait_ready().await?;
|
||||
|
||||
Ok(service)
|
||||
|
|
@ -103,7 +105,7 @@ impl MinioService {
|
|||
secret_key: &str,
|
||||
) -> Result<Self> {
|
||||
let bin_path = Self::find_minio_binary()?;
|
||||
log::info!("Using MinIO from: {bin_path:?}");
|
||||
log::info!("Using MinIO from: {}", bin_path.display());
|
||||
|
||||
let data_path = PathBuf::from(data_dir).join("minio");
|
||||
ensure_dir(&data_path)?;
|
||||
|
|
@ -120,13 +122,13 @@ impl MinioService {
|
|||
secret_key: secret_key.to_string(),
|
||||
};
|
||||
|
||||
service.start_server().await?;
|
||||
service.start_server()?;
|
||||
service.wait_ready().await?;
|
||||
|
||||
Ok(service)
|
||||
}
|
||||
|
||||
async fn start_server(&mut self) -> Result<()> {
|
||||
fn start_server(&mut self) -> Result<()> {
|
||||
log::info!(
|
||||
"Starting MinIO on port {} (console: {})",
|
||||
self.api_port,
|
||||
|
|
@ -192,11 +194,7 @@ impl MinioService {
|
|||
.output();
|
||||
|
||||
let output = Command::new(&mc)
|
||||
.args([
|
||||
"mb",
|
||||
"--ignore-existing",
|
||||
&format!("{alias_name}/{name}"),
|
||||
])
|
||||
.args(["mb", "--ignore-existing", &format!("{alias_name}/{name}")])
|
||||
.output()?;
|
||||
|
||||
if !output.status.success() {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
|
||||
use super::{check_tcp_port, ensure_dir, wait_for, HEALTH_CHECK_INTERVAL, HEALTH_CHECK_TIMEOUT};
|
||||
use anyhow::{Context, Result};
|
||||
use nix::sys::signal::{kill, Signal};
|
||||
|
|
@ -50,6 +49,7 @@ impl RedisService {
|
|||
Ok(service)
|
||||
}
|
||||
|
||||
#[allow(clippy::unused_async)]
|
||||
async fn start_server(&mut self) -> Result<()> {
|
||||
log::info!("Starting Redis on port {}", self.port);
|
||||
|
||||
|
|
@ -125,6 +125,7 @@ impl RedisService {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
#[allow(clippy::unused_async)]
|
||||
pub async fn execute(&self, args: &[&str]) -> Result<String> {
|
||||
let redis_cli = Self::find_cli_binary()?;
|
||||
|
||||
|
|
@ -182,7 +183,10 @@ impl RedisService {
|
|||
if result.is_empty() || result == "(empty list or set)" {
|
||||
Ok(Vec::new())
|
||||
} else {
|
||||
Ok(result.lines().map(std::string::ToString::to_string).collect())
|
||||
Ok(result
|
||||
.lines()
|
||||
.map(std::string::ToString::to_string)
|
||||
.collect())
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -193,10 +197,7 @@ impl RedisService {
|
|||
|
||||
pub async fn publish(&self, channel: &str, message: &str) -> Result<i64> {
|
||||
let result = self.execute(&["PUBLISH", channel, message]).await?;
|
||||
let count = result
|
||||
.replace("(integer) ", "")
|
||||
.parse::<i64>()
|
||||
.unwrap_or(0);
|
||||
let count = result.replace("(integer) ", "").parse::<i64>().unwrap_or(0);
|
||||
Ok(count)
|
||||
}
|
||||
|
||||
|
|
@ -230,10 +231,7 @@ impl RedisService {
|
|||
|
||||
pub async fn llen(&self, key: &str) -> Result<i64> {
|
||||
let result = self.execute(&["LLEN", key]).await?;
|
||||
let len = result
|
||||
.replace("(integer) ", "")
|
||||
.parse::<i64>()
|
||||
.unwrap_or(0);
|
||||
let len = result.replace("(integer) ", "").parse::<i64>().unwrap_or(0);
|
||||
Ok(len)
|
||||
}
|
||||
|
||||
|
|
@ -271,19 +269,13 @@ impl RedisService {
|
|||
|
||||
pub async fn incr(&self, key: &str) -> Result<i64> {
|
||||
let result = self.execute(&["INCR", key]).await?;
|
||||
let val = result
|
||||
.replace("(integer) ", "")
|
||||
.parse::<i64>()
|
||||
.unwrap_or(0);
|
||||
let val = result.replace("(integer) ", "").parse::<i64>().unwrap_or(0);
|
||||
Ok(val)
|
||||
}
|
||||
|
||||
pub async fn decr(&self, key: &str) -> Result<i64> {
|
||||
let result = self.execute(&["DECR", key]).await?;
|
||||
let val = result
|
||||
.replace("(integer) ", "")
|
||||
.parse::<i64>()
|
||||
.unwrap_or(0);
|
||||
let val = result.replace("(integer) ", "").parse::<i64>().unwrap_or(0);
|
||||
Ok(val)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
|
||||
pub mod browser;
|
||||
pub mod pages;
|
||||
|
||||
|
|
@ -108,6 +107,7 @@ impl Locator {
|
|||
}
|
||||
|
||||
#[must_use]
|
||||
#[allow(clippy::match_same_arms)]
|
||||
pub fn to_css_selector(&self) -> String {
|
||||
match self {
|
||||
Self::Css(s) => s.clone(),
|
||||
|
|
|
|||
|
|
@ -156,11 +156,9 @@ async fn perform_logout(browser: &Browser, base_url: &str) -> Result<bool, Strin
|
|||
|
||||
for selector in &logout_selectors {
|
||||
let locator = Locator::css(selector);
|
||||
if browser.exists(locator.clone()).await {
|
||||
if browser.click(locator).await.is_ok() {
|
||||
tokio::time::sleep(Duration::from_secs(1)).await;
|
||||
break;
|
||||
}
|
||||
if browser.exists(locator.clone()).await && browser.click(locator).await.is_ok() {
|
||||
tokio::time::sleep(Duration::from_secs(1)).await;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -171,20 +169,19 @@ async fn perform_logout(browser: &Browser, base_url: &str) -> Result<bool, Strin
|
|||
|
||||
for selector in &logout_selectors {
|
||||
let locator = Locator::css(selector);
|
||||
if browser.exists(locator.clone()).await {
|
||||
if browser.click(locator).await.is_ok() {
|
||||
tokio::time::sleep(Duration::from_secs(1)).await;
|
||||
break;
|
||||
}
|
||||
if browser.exists(locator.clone()).await && browser.click(locator).await.is_ok() {
|
||||
tokio::time::sleep(Duration::from_secs(1)).await;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let current_url = browser.current_url().await.unwrap_or_default();
|
||||
let base_url_with_slash = format!("{base_url}/");
|
||||
let logged_out = current_url.contains("/login")
|
||||
|| current_url.contains("/logout")
|
||||
|| current_url == format!("{}/", base_url)
|
||||
|| current_url == base_url.to_string();
|
||||
|| current_url == base_url_with_slash
|
||||
|| current_url == base_url;
|
||||
|
||||
if logged_out {
|
||||
return Ok(true);
|
||||
|
|
@ -442,10 +439,10 @@ async fn test_session_persistence() {
|
|||
tokio::time::sleep(Duration::from_secs(1)).await;
|
||||
let current_url = browser.current_url().await.unwrap_or_default();
|
||||
|
||||
if !current_url.contains("/login") {
|
||||
println!("✓ Session persisted after page refresh");
|
||||
} else {
|
||||
if current_url.contains("/login") {
|
||||
eprintln!("✗ Session lost after refresh");
|
||||
} else {
|
||||
println!("✓ Session persisted after page refresh");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -454,10 +451,10 @@ async fn test_session_persistence() {
|
|||
tokio::time::sleep(Duration::from_secs(1)).await;
|
||||
let current_url = browser.current_url().await.unwrap_or_default();
|
||||
|
||||
if !current_url.contains("/login") {
|
||||
println!("✓ Session maintained across navigation");
|
||||
} else {
|
||||
if current_url.contains("/login") {
|
||||
eprintln!("✗ Session lost during navigation");
|
||||
} else {
|
||||
println!("✓ Session maintained across navigation");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -1,41 +1,36 @@
|
|||
use super::{should_run_e2e_tests, E2ETestContext};
|
||||
use anyhow::{bail, Result};
|
||||
use bottest::prelude::*;
|
||||
use bottest::web::Locator;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_chat_hi() {
|
||||
async fn test_chat_hi() -> Result<()> {
|
||||
if !should_run_e2e_tests() {
|
||||
eprintln!("Skipping: E2E tests disabled");
|
||||
return;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let ctx = match E2ETestContext::setup_with_browser().await {
|
||||
Ok(ctx) => ctx,
|
||||
Err(e) => {
|
||||
eprintln!("Test failed: {}", e);
|
||||
panic!("Failed to setup E2E context: {}", e);
|
||||
}
|
||||
};
|
||||
let ctx = E2ETestContext::setup_with_browser().await?;
|
||||
|
||||
if !ctx.has_browser() {
|
||||
ctx.close().await;
|
||||
panic!("Browser not available - cannot run E2E test");
|
||||
bail!("Browser not available - cannot run E2E test");
|
||||
}
|
||||
|
||||
if ctx.ui.is_none() {
|
||||
ctx.close().await;
|
||||
panic!("BotUI not available - chat tests require botui running on port 3000");
|
||||
bail!("BotUI not available - chat tests require botui running on port 3000");
|
||||
}
|
||||
|
||||
let browser = ctx.browser.as_ref().unwrap();
|
||||
let ui_url = ctx.ui.as_ref().unwrap().url.clone();
|
||||
let chat_url = format!("{}/#chat", ui_url);
|
||||
|
||||
println!("🌐 Navigating to: {}", chat_url);
|
||||
println!("🌐 Navigating to: {chat_url}");
|
||||
|
||||
if let Err(e) = browser.goto(&chat_url).await {
|
||||
ctx.close().await;
|
||||
panic!("Failed to navigate to chat: {}", e);
|
||||
bail!("Failed to navigate to chat: {e}");
|
||||
}
|
||||
|
||||
println!("⏳ Waiting for page to load...");
|
||||
|
|
@ -47,10 +42,10 @@ async fn test_chat_hi() {
|
|||
for attempt in 1..=10 {
|
||||
if browser.exists(input.clone()).await {
|
||||
found_input = true;
|
||||
println!("✓ Chat input found (attempt {})", attempt);
|
||||
println!("✓ Chat input found (attempt {attempt})");
|
||||
break;
|
||||
}
|
||||
println!(" ... waiting for chat input (attempt {}/10)", attempt);
|
||||
println!(" ... waiting for chat input (attempt {attempt}/10)");
|
||||
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
|
||||
}
|
||||
|
||||
|
|
@ -61,27 +56,25 @@ async fn test_chat_hi() {
|
|||
}
|
||||
if let Ok(source) = browser.page_source().await {
|
||||
let preview: String = source.chars().take(2000).collect();
|
||||
println!("Page source preview:\n{}", preview);
|
||||
println!("Page source preview:\n{preview}");
|
||||
}
|
||||
ctx.close().await;
|
||||
panic!("Chat input not found after 10 attempts");
|
||||
bail!("Chat input not found after 10 attempts");
|
||||
}
|
||||
|
||||
println!("⌨️ Typing 'hi'...");
|
||||
if let Err(e) = browser.type_text(input.clone(), "hi").await {
|
||||
ctx.close().await;
|
||||
panic!("Failed to type: {}", e);
|
||||
bail!("Failed to type: {e}");
|
||||
}
|
||||
|
||||
let send_btn = Locator::css("#sendBtn, #ai-send, .ai-send, button[type='submit']");
|
||||
match browser.click(send_btn).await {
|
||||
Ok(_) => println!("✓ Message sent (click)"),
|
||||
Err(_) => {
|
||||
match browser.press_key(input, "Enter").await {
|
||||
Ok(_) => println!("✓ Message sent (Enter key)"),
|
||||
Err(e) => println!("⚠ Send may have failed: {}", e),
|
||||
}
|
||||
}
|
||||
Ok(()) => println!("✓ Message sent (click)"),
|
||||
Err(_) => match browser.press_key(input, "Enter").await {
|
||||
Ok(()) => println!("✓ Message sent (Enter key)"),
|
||||
Err(e) => println!("⚠ Send may have failed: {e}"),
|
||||
},
|
||||
}
|
||||
|
||||
println!("⏳ Waiting for bot response...");
|
||||
|
|
@ -105,29 +98,25 @@ async fn test_chat_hi() {
|
|||
|
||||
ctx.close().await;
|
||||
println!("✅ Chat test complete!");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_chat_page_loads() {
|
||||
async fn test_chat_page_loads() -> Result<()> {
|
||||
if !should_run_e2e_tests() {
|
||||
return;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let ctx = match E2ETestContext::setup_with_browser().await {
|
||||
Ok(ctx) => ctx,
|
||||
Err(e) => {
|
||||
panic!("Setup failed: {}", e);
|
||||
}
|
||||
};
|
||||
let ctx = E2ETestContext::setup_with_browser().await?;
|
||||
|
||||
if !ctx.has_browser() {
|
||||
ctx.close().await;
|
||||
panic!("Browser not available");
|
||||
bail!("Browser not available");
|
||||
}
|
||||
|
||||
if ctx.ui.is_none() {
|
||||
ctx.close().await;
|
||||
panic!("BotUI not available - chat tests require botui. Start it with: cd ../botui && cargo run");
|
||||
bail!("BotUI not available - chat tests require botui. Start it with: cd ../botui && cargo run");
|
||||
}
|
||||
|
||||
let browser = ctx.browser.as_ref().unwrap();
|
||||
|
|
@ -136,7 +125,7 @@ async fn test_chat_page_loads() {
|
|||
|
||||
if let Err(e) = browser.goto(&chat_url).await {
|
||||
ctx.close().await;
|
||||
panic!("Navigation failed: {}", e);
|
||||
bail!("Navigation failed: {e}");
|
||||
}
|
||||
|
||||
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
|
||||
|
|
@ -149,9 +138,10 @@ async fn test_chat_page_loads() {
|
|||
let _ = std::fs::write("/tmp/bottest-fail.png", &s);
|
||||
}
|
||||
ctx.close().await;
|
||||
panic!("Chat not loaded: {}", e);
|
||||
bail!("Chat not loaded: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
ctx.close().await;
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -773,7 +773,10 @@ async fn test_with_fixtures() {
|
|||
|
||||
match ctx.ctx.insert_user(&user).await {
|
||||
Ok(_) => println!("Inserted test user: {}", user.email),
|
||||
Err(e) => eprintln!("Could not insert user (DB may not be directly accessible): {}", e),
|
||||
Err(e) => eprintln!(
|
||||
"Could not insert user (DB may not be directly accessible): {}",
|
||||
e
|
||||
),
|
||||
}
|
||||
|
||||
match ctx.ctx.insert_bot(&bot).await {
|
||||
|
|
@ -822,12 +825,10 @@ async fn test_mock_services_available() {
|
|||
Ok(_pool) => println!("✓ Connected to existing PostgreSQL"),
|
||||
Err(e) => eprintln!("Could not connect to existing PostgreSQL: {}", e),
|
||||
}
|
||||
} else if ctx.ctx.postgres().is_some() {
|
||||
println!("✓ PostgreSQL is managed by harness");
|
||||
} else {
|
||||
if ctx.ctx.postgres().is_some() {
|
||||
println!("✓ PostgreSQL is managed by harness");
|
||||
} else {
|
||||
eprintln!("PostgreSQL should be started in fresh stack mode");
|
||||
}
|
||||
eprintln!("PostgreSQL should be started in fresh stack mode");
|
||||
}
|
||||
|
||||
ctx.close().await;
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ async fn is_service_running(url: &str) -> bool {
|
|||
.build()
|
||||
.unwrap_or_default();
|
||||
|
||||
if let Ok(resp) = client.get(&format!("{}/health", url)).send().await {
|
||||
if let Ok(resp) = client.get(format!("{url}/health")).send().await {
|
||||
if resp.status().is_success() {
|
||||
return true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,25 +36,6 @@ async fn get_test_server() -> Option<(Option<TestContext>, String)> {
|
|||
}
|
||||
}
|
||||
|
||||
fn is_server_available_sync() -> bool {
|
||||
if std::env::var("SKIP_INTEGRATION_TESTS").is_ok() {
|
||||
return false;
|
||||
}
|
||||
|
||||
if let Some(url) = external_server_url() {
|
||||
let client = reqwest::blocking::Client::builder()
|
||||
.timeout(Duration::from_secs(2))
|
||||
.build()
|
||||
.ok();
|
||||
|
||||
if let Some(client) = client {
|
||||
return client.get(&url).send().is_ok();
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
macro_rules! skip_if_no_server {
|
||||
($base_url:expr) => {
|
||||
if $base_url.is_none() {
|
||||
|
|
@ -658,7 +639,7 @@ async fn test_mock_llm_assertions() {
|
|||
|
||||
let client = reqwest::Client::new();
|
||||
let _ = client
|
||||
.post(&format!("{}/v1/chat/completions", mock_llm.url()))
|
||||
.post(format!("{}/v1/chat/completions", mock_llm.url()))
|
||||
.json(&serde_json::json!({
|
||||
"model": "gpt-4",
|
||||
"messages": [{"role": "user", "content": "test"}]
|
||||
|
|
@ -685,7 +666,7 @@ async fn test_mock_llm_error_simulation() {
|
|||
|
||||
let client = reqwest::Client::new();
|
||||
let response = client
|
||||
.post(&format!("{}/v1/chat/completions", mock_llm.url()))
|
||||
.post(format!("{}/v1/chat/completions", mock_llm.url()))
|
||||
.json(&serde_json::json!({
|
||||
"model": "gpt-4",
|
||||
"messages": [{"role": "user", "content": "test"}]
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
use rhai::Engine;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
|
||||
fn create_basic_engine() -> Engine {
|
||||
let mut engine = Engine::new();
|
||||
|
||||
|
|
@ -135,9 +134,8 @@ impl InputProvider {
|
|||
fn create_conversation_engine(output: OutputCollector, input: InputProvider) -> Engine {
|
||||
let mut engine = create_basic_engine();
|
||||
|
||||
let output_clone = output.clone();
|
||||
engine.register_fn("TALK", move |msg: &str| {
|
||||
output_clone.add_message(msg.to_string());
|
||||
output.add_message(msg.to_string());
|
||||
});
|
||||
|
||||
engine.register_fn("HEAR", move || -> String { input.next_input() });
|
||||
|
|
@ -145,7 +143,6 @@ fn create_conversation_engine(output: OutputCollector, input: InputProvider) ->
|
|||
engine
|
||||
}
|
||||
|
||||
|
||||
#[test]
|
||||
fn test_string_concatenation_in_engine() {
|
||||
let engine = create_basic_engine();
|
||||
|
|
@ -208,7 +205,6 @@ fn test_replace_function() {
|
|||
assert_eq!(result, "bbb");
|
||||
}
|
||||
|
||||
|
||||
#[test]
|
||||
fn test_math_operations_chain() {
|
||||
let engine = create_basic_engine();
|
||||
|
|
@ -264,14 +260,13 @@ fn test_val_function() {
|
|||
let result: f64 = engine.eval(r#"VAL("42")"#).unwrap();
|
||||
assert!((result - 42.0).abs() < f64::EPSILON);
|
||||
|
||||
let result: f64 = engine.eval(r#"VAL("3.14")"#).unwrap();
|
||||
assert!((result - 3.14).abs() < f64::EPSILON);
|
||||
let result: f64 = engine.eval(r#"VAL("3.5")"#).unwrap();
|
||||
assert!((result - 3.5).abs() < f64::EPSILON);
|
||||
|
||||
let result: f64 = engine.eval(r#"VAL("invalid")"#).unwrap();
|
||||
assert!((result - 0.0).abs() < f64::EPSILON);
|
||||
}
|
||||
|
||||
|
||||
#[test]
|
||||
fn test_talk_output() {
|
||||
let output = OutputCollector::new();
|
||||
|
|
@ -393,19 +388,18 @@ fn test_keyword_detection() {
|
|||
assert_eq!(messages[0], "I can help you! What do you need?");
|
||||
}
|
||||
|
||||
|
||||
#[test]
|
||||
fn test_variable_assignment() {
|
||||
let engine = create_basic_engine();
|
||||
|
||||
let result: i64 = engine
|
||||
.eval(
|
||||
r#"
|
||||
r"
|
||||
let x = 10;
|
||||
let y = 20;
|
||||
let z = x + y;
|
||||
z
|
||||
"#,
|
||||
",
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(result, 30);
|
||||
|
|
@ -442,7 +436,6 @@ fn test_numeric_expressions() {
|
|||
assert_eq!(result, 12);
|
||||
}
|
||||
|
||||
|
||||
#[test]
|
||||
fn test_for_loop() {
|
||||
let output = OutputCollector::new();
|
||||
|
|
@ -472,7 +465,7 @@ fn test_while_loop() {
|
|||
|
||||
let result: i64 = engine
|
||||
.eval(
|
||||
r#"
|
||||
r"
|
||||
let count = 0;
|
||||
let sum = 0;
|
||||
while count < 5 {
|
||||
|
|
@ -480,21 +473,19 @@ fn test_while_loop() {
|
|||
count = count + 1;
|
||||
}
|
||||
sum
|
||||
"#,
|
||||
",
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(result, 10);
|
||||
}
|
||||
|
||||
|
||||
#[test]
|
||||
fn test_division_by_zero() {
|
||||
let engine = create_basic_engine();
|
||||
|
||||
let result = engine.eval::<f64>("10.0 / 0.0");
|
||||
match result {
|
||||
Ok(val) => assert!(val.is_infinite() || val.is_nan()),
|
||||
Err(_) => (),
|
||||
if let Ok(val) = result {
|
||||
assert!(val.is_infinite() || val.is_nan());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -514,7 +505,6 @@ fn test_type_mismatch() {
|
|||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
|
||||
#[test]
|
||||
fn test_greeting_script_logic() {
|
||||
let output = OutputCollector::new();
|
||||
|
|
@ -608,7 +598,6 @@ fn test_echo_bot_logic() {
|
|||
assert_eq!(messages[2], "You said: How are you?");
|
||||
}
|
||||
|
||||
|
||||
#[test]
|
||||
fn test_order_lookup_simulation() {
|
||||
let output = OutputCollector::new();
|
||||
|
|
|
|||
|
|
@ -154,37 +154,37 @@ async fn test_query_result_types() {
|
|||
use diesel::sql_query;
|
||||
|
||||
#[derive(QueryableByName)]
|
||||
struct TypeTestResult {
|
||||
struct TypeTestRow {
|
||||
#[diesel(sql_type = diesel::sql_types::Integer)]
|
||||
int_val: i32,
|
||||
integer: i32,
|
||||
#[diesel(sql_type = diesel::sql_types::BigInt)]
|
||||
bigint_val: i64,
|
||||
bigint: i64,
|
||||
#[diesel(sql_type = diesel::sql_types::Text)]
|
||||
text_val: String,
|
||||
text: String,
|
||||
#[diesel(sql_type = diesel::sql_types::Bool)]
|
||||
bool_val: bool,
|
||||
flag: bool,
|
||||
#[diesel(sql_type = diesel::sql_types::Double)]
|
||||
float_val: f64,
|
||||
decimal: f64,
|
||||
}
|
||||
|
||||
let mut conn = pool.get().expect("Failed to get connection");
|
||||
let result: Vec<TypeTestResult> = sql_query(
|
||||
let result: Vec<TypeTestRow> = sql_query(
|
||||
"SELECT
|
||||
42 as int_val,
|
||||
9223372036854775807::bigint as bigint_val,
|
||||
'hello' as text_val,
|
||||
true as bool_val,
|
||||
3.125 as float_val",
|
||||
42 as integer,
|
||||
9223372036854775807::bigint as bigint,
|
||||
'hello' as text,
|
||||
true as flag,
|
||||
3.125 as decimal",
|
||||
)
|
||||
.load(&mut conn)
|
||||
.expect("Query failed");
|
||||
|
||||
assert_eq!(result.len(), 1);
|
||||
assert_eq!(result[0].int_val, 42);
|
||||
assert_eq!(result[0].bigint_val, 9223372036854775807_i64);
|
||||
assert_eq!(result[0].text_val, "hello");
|
||||
assert!(result[0].bool_val);
|
||||
assert!((result[0].float_val - 3.125).abs() < 0.0001);
|
||||
assert_eq!(result[0].integer, 42);
|
||||
assert_eq!(result[0].bigint, 9_223_372_036_854_775_807_i64);
|
||||
assert_eq!(result[0].text, "hello");
|
||||
assert!(result[0].flag);
|
||||
assert!((result[0].decimal - 3.125).abs() < 0.0001);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
|
|
|||
|
|
@ -29,16 +29,6 @@ pub fn should_run_integration_tests() -> bool {
|
|||
true
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! skip_if_no_services {
|
||||
() => {
|
||||
if !crate::integration::should_run_integration_tests() {
|
||||
eprintln!("Skipping integration test: SKIP_INTEGRATION_TESTS is set");
|
||||
return;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_harness_database_only() {
|
||||
if !should_run_integration_tests() {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
#[test]
|
||||
fn test_unit_module_loads() {
|
||||
// Unit tests are now inline in botserver source files
|
||||
// This module is kept for integration test infrastructure
|
||||
assert!(true);
|
||||
let module_name = module_path!();
|
||||
assert!(module_name.contains("unit"));
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue