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