diff --git a/bin/fxt/benches/client_performance.rs b/bin/fxt/benches/client_performance.rs index 6d6e2845a..30b587532 100644 --- a/bin/fxt/benches/client_performance.rs +++ b/bin/fxt/benches/client_performance.rs @@ -284,7 +284,7 @@ fn bench_error_scenarios(c: &mut Criterion) { // Connection error simulation group.bench_function("connection_error", |b| { b.iter(|| { - let error = TliError::Connection(black_box("Connection refused".to_string())); + let error = FxtError::Connection(black_box("Connection refused".to_string())); let formatted = format!("{}", error); black_box(formatted) }) @@ -293,7 +293,7 @@ fn bench_error_scenarios(c: &mut Criterion) { // Invalid request error group.bench_function("invalid_request_error", |b| { b.iter(|| { - let error = TliError::InvalidRequest(black_box("Invalid order quantity".to_string())); + let error = FxtError::InvalidRequest(black_box("Invalid order quantity".to_string())); let formatted = format!("{}", error); black_box(formatted) }) @@ -303,7 +303,7 @@ fn bench_error_scenarios(c: &mut Criterion) { group.bench_function("not_connected_error", |b| { b.iter(|| { let error = - TliError::Connection(black_box("Trading service not connected".to_string())); + FxtError::Connection(black_box("Trading service not connected".to_string())); let formatted = format!("{}", error); black_box(formatted) }) @@ -314,8 +314,8 @@ fn bench_error_scenarios(c: &mut Criterion) { b.iter(|| { let io_error = std::io::Error::new(std::io::ErrorKind::ConnectionRefused, "Connection refused"); - let tli_error: TliError = io_error.into(); - let formatted = format!("{}", black_box(tli_error)); + let fxt_error: FxtError = io_error.into(); + let formatted = format!("{}", black_box(fxt_error)); black_box(formatted) }) }); diff --git a/bin/fxt/benches/configuration_benchmarks.rs b/bin/fxt/benches/configuration_benchmarks.rs index 456a7f143..e11c25714 100644 --- a/bin/fxt/benches/configuration_benchmarks.rs +++ b/bin/fxt/benches/configuration_benchmarks.rs @@ -410,7 +410,7 @@ fn bench_error_handling(c: &mut Criterion) { // Result creation and matching group.bench_function("success_result", |b| { b.iter(|| { - let result: TliResult = Ok(black_box(42)); + let result: FxtResult = Ok(black_box(42)); match result { Ok(value) => black_box(value), Err(_) => 0, @@ -420,7 +420,7 @@ fn bench_error_handling(c: &mut Criterion) { group.bench_function("error_result", |b| { b.iter(|| { - let result: TliResult = Err(TliError::InvalidRequest("test error".to_string())); + let result: FxtResult = Err(FxtError::InvalidRequest("test error".to_string())); match result { Ok(value) => value, Err(_) => black_box(0), @@ -431,14 +431,14 @@ fn bench_error_handling(c: &mut Criterion) { // Error creation group.bench_function("error_creation", |b| { b.iter(|| { - let error = TliError::Connection(black_box("Connection failed".to_string())); + let error = FxtError::Connection(black_box("Connection failed".to_string())); black_box(error) }) }); // Error message formatting group.bench_function("error_formatting", |b| { - let error = TliError::InvalidSymbol("TEST".to_string()); + let error = FxtError::InvalidSymbol("TEST".to_string()); b.iter(|| { let formatted = format!("{}", black_box(&error)); black_box(formatted) diff --git a/bin/fxt/build.rs b/bin/fxt/build.rs index 74c0694ce..fee1e70ea 100644 --- a/bin/fxt/build.rs +++ b/bin/fxt/build.rs @@ -1,7 +1,7 @@ fn main() -> Result<(), Box> { // proto/ at workspace root — single source of truth for all services. // Note: fxt_trading.proto (package foxhunt.tli) is excluded — it is the - // legacy fat-client proto kept only for API Gateway backward compatibility. + // legacy fat-client proto kept for wire compatibility (package name retained). let proto_root = "../../proto"; let protos: Vec = [ "trading", @@ -14,7 +14,6 @@ fn main() -> Result<(), Box> { "monitoring", "risk", "data_acquisition", - "config_service", ] .iter() .map(|p| format!("{proto_root}/{p}.proto")) @@ -40,7 +39,6 @@ fn main() -> Result<(), Box> { "monitoring", "risk", "data_acquisition", - "config_service", ] { println!("cargo:rerun-if-changed={proto_root}/{proto}.proto"); } diff --git a/bin/fxt/src/auth/jwt_generator.rs b/bin/fxt/src/auth/jwt_generator.rs index 0df5d496c..043dbaeb1 100644 --- a/bin/fxt/src/auth/jwt_generator.rs +++ b/bin/fxt/src/auth/jwt_generator.rs @@ -51,7 +51,7 @@ impl Default for JwtConfig { let resolved_secret = std::env::var("JWT_SECRET") .ok() .or_else(|| { - crate::config::TliConfig::load() + crate::config::FxtConfig::load() .ok() .and_then(|c| c.jwt_secret) }) diff --git a/bin/fxt/src/auth/token_manager.rs b/bin/fxt/src/auth/token_manager.rs index 2bbacd0b0..3c7678241 100644 --- a/bin/fxt/src/auth/token_manager.rs +++ b/bin/fxt/src/auth/token_manager.rs @@ -1,7 +1,7 @@ //! JWT token management with automatic refresh //! //! Manages access tokens and refresh tokens via OS keyring for secure persistence. -//! Since TLI is a CLI tool (not a long-running service), tokens are read from +//! Since FXT is a CLI tool (not a long-running service), tokens are read from //! keyring on each access rather than cached in memory. use anyhow::{Context, Result}; @@ -11,6 +11,35 @@ use std::sync::Arc; use std::time::{Duration, SystemTime, UNIX_EPOCH}; use tokio::sync::RwLock; +/// Recursively copy a directory tree (used for token storage migration fallback). +fn copy_dir_recursive(src: &std::path::Path, dst: &std::path::Path) -> Result<()> { + std::fs::create_dir_all(dst) + .with_context(|| format!("Failed to create directory {}", dst.display()))?; + for dir_entry in std::fs::read_dir(src) + .with_context(|| format!("Failed to read directory {}", src.display()))? + { + let entry = dir_entry.context("Failed to read directory entry")?; + let src_path = entry.path(); + let dst_path = dst.join(entry.file_name()); + if src_path.is_dir() { + copy_dir_recursive(&src_path, &dst_path)?; + } else { + std::fs::copy(&src_path, &dst_path).with_context(|| { + format!( + "Failed to copy {} to {}", + src_path.display(), + dst_path.display() + ) + })?; + } + } + Ok(()) +} + +/// Buffer (in seconds) before actual JWT expiry at which we consider the token expired. +/// This ensures we refresh proactively rather than racing against clock skew. +const TOKEN_REFRESH_BUFFER_SECS: u64 = 60; + /// JWT token information #[derive(Debug, Clone, Serialize, Deserialize)] pub struct TokenInfo { @@ -25,15 +54,14 @@ pub struct TokenInfo { impl TokenInfo { /// Check if the access token is expired or nearing expiration /// - /// Returns true if the token expires within the next 60 seconds + /// Returns true if the token expires within [`TOKEN_REFRESH_BUFFER_SECS`] seconds pub fn is_expired(&self) -> bool { let now = SystemTime::now() .duration_since(UNIX_EPOCH) .unwrap_or(Duration::from_secs(0)) .as_secs(); - // Consider expired if within 60 seconds of expiration - self.expires_at <= now + 60 + self.expires_at <= now + TOKEN_REFRESH_BUFFER_SECS } /// Get remaining time until token expiration @@ -64,6 +92,7 @@ fn extract_token_expiry(token: &str) -> Result { validation.validate_exp = false; validation.validate_aud = false; // Disable audience validation for flexibility + // Key value is irrelevant — signature validation is disabled above let token_data = decode::(token, &DecodingKey::from_secret(b"dummy"), &validation) .context("Failed to decode JWT token")?; @@ -103,7 +132,7 @@ impl KeyringTokenStorage { /// Create a new keyring token storage /// /// # Arguments - /// * `service_name` - Application service name (e.g., "foxhunt-tli") + /// * `service_name` - Application service name (e.g., "foxhunt-fxt") /// /// * `username` - Username for keyring entry pub const fn new(service_name: String, username: String) -> Self { @@ -152,7 +181,7 @@ impl TokenStorage for KeyringTokenStorage { let owned_token = token.to_owned(); tokio::task::spawn_blocking(move || { - let entry = keyring::Entry::new("foxhunt-tli-access", "default") + let entry = keyring::Entry::new("foxhunt-fxt-access", "default") .context("Failed to create keyring entry for access token")?; entry @@ -168,7 +197,7 @@ impl TokenStorage for KeyringTokenStorage { async fn get_access_token(&self) -> Result> { tokio::task::spawn_blocking(move || { - let entry = keyring::Entry::new("foxhunt-tli-access", "default") + let entry = keyring::Entry::new("foxhunt-fxt-access", "default") .context("Failed to create keyring entry for access token")?; match entry.get_password() { @@ -242,7 +271,7 @@ impl TokenStorage for KeyringTokenStorage { async fn clear_access_token(&self) -> Result<()> { tokio::task::spawn_blocking(move || { - let entry = keyring::Entry::new("foxhunt-tli-access", "default") + let entry = keyring::Entry::new("foxhunt-fxt-access", "default") .context("Failed to create keyring entry for access token")?; match entry.delete_credential() { @@ -266,7 +295,7 @@ impl TokenStorage for KeyringTokenStorage { /// object cannot be retrieved by a different Entry object. /// /// Security notes: -/// - Files stored in `~/.config/foxhunt-tli/tokens/` +/// - Files stored in `~/.config/foxhunt-fxt/tokens/` /// - Directory permissions: 700 (owner read/write/execute only) /// - File permissions: 600 (owner read/write only) /// - AES-256-GCM encryption provides production-grade security @@ -281,10 +310,27 @@ impl FileTokenStorage { /// /// Creates token directory with 700 permissions if it doesn't exist. pub fn new() -> Result { - let token_dir = dirs::config_dir() - .context("Cannot determine config directory")? - .join("foxhunt-tli") - .join("tokens"); + let config_base = dirs::config_dir().context("Cannot determine config directory")?; + let new_dir = config_base.join("foxhunt-fxt"); + let old_dir = config_base.join("foxhunt-tli"); + + // Migrate from legacy foxhunt-tli directory if it exists and foxhunt-fxt does not + if !new_dir.exists() && old_dir.exists() { + tracing::info!( + "Migrating token storage from {} to {}", + old_dir.display(), + new_dir.display() + ); + if let Err(e) = std::fs::rename(&old_dir, &new_dir) { + // rename() fails across mount points; fall back to copy + remove + tracing::warn!("rename() failed ({}), falling back to copy", e); + copy_dir_recursive(&old_dir, &new_dir)?; + // Best-effort cleanup of old dir; failure is non-fatal + drop(std::fs::remove_dir_all(&old_dir)); + } + } + + let token_dir = new_dir.join("tokens"); // Create directory with 700 permissions (owner only) std::fs::create_dir_all(&token_dir).context("Failed to create token directory")?; @@ -410,9 +456,24 @@ impl FileTokenStorage { impl Default for FileTokenStorage { fn default() -> Self { - // FileTokenStorage::new() creates dirs on the filesystem; Default can't return Result - #[allow(clippy::expect_used)] - Self::new().expect("Failed to create FileTokenStorage") + // Try the normal path first; fall back to /tmp/foxhunt-fxt/tokens/ if it fails. + // This avoids a panic when the user's config directory is unavailable. + Self::new().unwrap_or_else(|e| { + tracing::warn!( + "FileTokenStorage::new() failed ({}), falling back to /tmp/foxhunt-fxt/tokens/", + e + ); + let fallback = std::path::PathBuf::from("/tmp/foxhunt-fxt/tokens"); + Self::with_directory(fallback).unwrap_or_else(|e2| { + // Last resort: construct without creating dirs -- will fail at read/write + // time rather than panicking here. + tracing::error!("Fallback token storage also failed: {}", e2); + Self { + token_dir: std::path::PathBuf::from("/tmp/foxhunt-fxt/tokens"), + key_manager: std::sync::Mutex::new(crate::auth::key_manager::KeyManager::new()), + } + }) + }) } } @@ -561,7 +622,7 @@ impl TokenStorage for InMemoryTokenStorage { /// Authentication token manager /// -/// Manages JWT tokens via keyring storage. Since TLI is a CLI tool, +/// Manages JWT tokens via keyring storage. Since FXT is a CLI tool, /// tokens are read from keyring on each access rather than cached in memory. pub struct AuthTokenManager { /// Token storage backend diff --git a/bin/fxt/src/commands/config.rs b/bin/fxt/src/commands/config.rs index 756aa5f1c..9cb6a9457 100644 --- a/bin/fxt/src/commands/config.rs +++ b/bin/fxt/src/commands/config.rs @@ -250,7 +250,7 @@ impl ConfigCommand { } ConfigAction::Env => { - let config = crate::config::TliConfig::load().unwrap_or_default(); + let config = crate::config::FxtConfig::load().unwrap_or_default(); let config_path = dirs::home_dir() .map(|h| h.join(".foxhunt").join("config.toml")) .map(|p| p.display().to_string()) diff --git a/bin/fxt/src/commands/mcp.rs b/bin/fxt/src/commands/mcp.rs index 09dbcfb3d..b401cd2fc 100644 --- a/bin/fxt/src/commands/mcp.rs +++ b/bin/fxt/src/commands/mcp.rs @@ -6,6 +6,7 @@ use anyhow::Result; use clap::Parser; +use crate::grpc::FoxhuntClient; use crate::mcp::server::McpServer; /// Start MCP server (JSON-RPC 2.0 on stdin/stdout) @@ -24,7 +25,8 @@ impl McpCommand { /// Start the MCP server that exposes Foxhunt operations as tools /// for LLM agents (e.g. Claude, Cursor). pub async fn execute(&self) -> Result<()> { - let server = McpServer::new(self.api_url.clone()); + let client = FoxhuntClient::connect(&self.api_url).await?; + let server = McpServer::new(self.api_url.clone(), client); server.run().await } } diff --git a/bin/fxt/src/config.rs b/bin/fxt/src/config.rs index 44bffd1f4..b97ae31b7 100644 --- a/bin/fxt/src/config.rs +++ b/bin/fxt/src/config.rs @@ -39,6 +39,7 @@ fn default_ibkr_host() -> String { fn default_ibkr_port() -> u16 { 4004 } +// Default IBKR client ID (99 = non-production default) fn default_ibkr_client_id() -> i32 { 99 } @@ -83,7 +84,7 @@ impl Default for BrokerConfig { /// Settings can be loaded from `~/.foxhunt/config.toml` and overridden /// by CLI arguments or environment variables. #[derive(Debug, Serialize, Deserialize)] -pub struct TliConfig { +pub struct FxtConfig { /// API Gateway URL (default: ) #[serde(default = "default_api_gateway_url")] pub api_gateway_url: String, @@ -117,7 +118,7 @@ fn default_token_storage() -> String { "keyring".to_owned() } -impl Default for TliConfig { +impl Default for FxtConfig { fn default() -> Self { Self { api_gateway_url: default_api_gateway_url(), @@ -129,7 +130,7 @@ impl Default for TliConfig { } } -impl TliConfig { +impl FxtConfig { /// Load config from ~/.foxhunt/config.toml /// /// Returns default configuration if file doesn't exist. @@ -137,9 +138,9 @@ impl TliConfig { /// /// # Examples /// ```no_run - /// use fxt::config::TliConfig; + /// use fxt::config::FxtConfig; /// - /// let config = TliConfig::load().unwrap(); + /// let config = FxtConfig::load().unwrap(); /// println!("API Gateway: {}", config.api_gateway_url); /// ``` pub fn load() -> Result { @@ -164,9 +165,9 @@ impl TliConfig { /// /// # Examples /// ```no_run - /// use fxt::config::TliConfig; + /// use fxt::config::FxtConfig; /// - /// let config = TliConfig { + /// let config = FxtConfig { /// api_gateway_url: "http://localhost:50051".to_string(), /// log_level: "debug".to_string(), /// token_storage: "keyring".to_string(), @@ -208,7 +209,7 @@ mod tests { #[test] fn test_default_config() { // Test that Default trait provides correct values - let config = TliConfig::default(); + let config = FxtConfig::default(); assert_eq!(config.api_gateway_url, "https://api.fxhnt.ai"); assert_eq!(config.log_level, "info"); assert_eq!(config.token_storage, "keyring"); @@ -216,7 +217,7 @@ mod tests { #[test] fn test_config_serialization() { - let config = TliConfig { + let config = FxtConfig { api_gateway_url: "http://example.com:50051".to_owned(), log_level: "debug".to_owned(), token_storage: "file".to_owned(), @@ -225,7 +226,7 @@ mod tests { }; let toml_str = toml::to_string(&config).unwrap(); - let parsed: TliConfig = toml::from_str(&toml_str).unwrap(); + let parsed: FxtConfig = toml::from_str(&toml_str).unwrap(); assert_eq!(config.api_gateway_url, parsed.api_gateway_url); assert_eq!(config.log_level, parsed.log_level); @@ -236,7 +237,7 @@ mod tests { fn test_load_config_succeeds() { // load() should always succeed: returns file contents if present, defaults otherwise. // Default-value assertions live in test_serde_defaults and test_default_config. - let result = TliConfig::load(); + let result = FxtConfig::load(); assert!(result.is_ok()); } @@ -244,7 +245,7 @@ mod tests { fn test_serde_defaults() { // Test that empty TOML string deserializes to defaults let empty_toml = ""; - let config: TliConfig = toml::from_str(empty_toml).unwrap(); + let config: FxtConfig = toml::from_str(empty_toml).unwrap(); assert_eq!(config.api_gateway_url, "https://api.fxhnt.ai"); assert_eq!(config.log_level, "info"); assert_eq!(config.token_storage, "keyring"); @@ -263,7 +264,7 @@ mod tests { port = 4002 client_id = 42 "#; - let config: TliConfig = toml::from_str(toml_str).unwrap(); + let config: FxtConfig = toml::from_str(toml_str).unwrap(); assert_eq!(config.broker.gateway_url, "http://broker-gw:50060"); assert_eq!(config.broker.ibkr.host, "10.0.0.5"); assert_eq!(config.broker.ibkr.port, 4002); @@ -272,7 +273,7 @@ mod tests { #[test] fn test_config_broker_defaults() { - let config: TliConfig = toml::from_str("").unwrap(); + let config: FxtConfig = toml::from_str("").unwrap(); assert_eq!(config.broker.gateway_url, "http://localhost:50056"); assert_eq!(config.broker.ibkr.host, "127.0.0.1"); assert_eq!(config.broker.ibkr.port, 4004); diff --git a/bin/fxt/src/error.rs b/bin/fxt/src/error.rs index 8c8645766..54a08505c 100644 --- a/bin/fxt/src/error.rs +++ b/bin/fxt/src/error.rs @@ -7,7 +7,7 @@ use thiserror::Error; /// Represents all possible errors that can occur in the FXT client system, /// including network connectivity, configuration, validation, and protocol errors. #[derive(Error, Debug)] -pub enum TliError { +pub enum FxtError { /// Network connection or gRPC transport errors /// /// Occurs when unable to establish or maintain connections to trading services, @@ -81,7 +81,7 @@ pub enum TliError { /// Result type alias for FXT operations /// -/// Standard Result type using `TliError` as the error variant. +/// Standard Result type using `FxtError` as the error variant. /// /// Used throughout the FXT codebase for consistent error handling. -pub type TliResult = Result; +pub type FxtResult = Result; diff --git a/bin/fxt/src/grpc.rs b/bin/fxt/src/grpc.rs index 35c639388..c4edee84e 100644 --- a/bin/fxt/src/grpc.rs +++ b/bin/fxt/src/grpc.rs @@ -176,17 +176,4 @@ impl FoxhuntClient { ) } - /// API Gateway configuration service client (package `foxhunt.config`). - /// - /// Distinct from [`Self::config`] which targets the trading service's - /// configuration proto (package `config`). - pub fn config_service( - &self, - ) -> crate::proto::config_service::configuration_service_client::ConfigurationServiceClient - { - crate::proto::config_service::configuration_service_client::ConfigurationServiceClient::with_interceptor( - self.channel.clone(), - self.interceptor.clone(), - ) - } } diff --git a/bin/fxt/src/lib.rs b/bin/fxt/src/lib.rs index ef22122ec..7d7b4c6a3 100644 --- a/bin/fxt/src/lib.rs +++ b/bin/fxt/src/lib.rs @@ -122,12 +122,4 @@ pub mod proto { tonic::include_proto!("data_acquisition"); } - /// Config service protobuf definitions (package `foxhunt.config`) - /// - /// Note: This is from `config_service.proto` (API Gateway config service), - /// distinct from `config.proto` (trading service config, package `config`). - #[allow(clippy::shadow_reuse, clippy::impl_trait_in_params, clippy::empty_structs_with_brackets)] - pub mod config_service { - tonic::include_proto!("foxhunt.config"); - } } diff --git a/bin/fxt/src/mcp/server.rs b/bin/fxt/src/mcp/server.rs index 9fd8e2309..573e62859 100644 --- a/bin/fxt/src/mcp/server.rs +++ b/bin/fxt/src/mcp/server.rs @@ -2,10 +2,13 @@ //! //! Reads one JSON object per line from stdin, dispatches to the //! appropriate handler, and writes the response to stdout. +//! Tool handlers make real gRPC calls to the API Gateway via [`FoxhuntClient`]. use serde_json::{json, Value}; use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader}; +use crate::grpc::FoxhuntClient; + use super::protocol::{ error_response, success_response, JsonRpcRequest, JsonRpcResponse, INVALID_PARAMS, INVALID_REQUEST, METHOD_NOT_FOUND, PARSE_ERROR, @@ -14,14 +17,16 @@ use super::tools; /// MCP server that exposes Foxhunt operations as tools. pub struct McpServer { - /// API Gateway URL used for future gRPC calls. + /// API Gateway URL (for diagnostic logging). api_url: String, + /// gRPC client connected to the API Gateway. + client: FoxhuntClient, } impl McpServer { /// Create a new MCP server targeting the given API endpoint. - pub fn new(api_url: String) -> Self { - Self { api_url } + pub fn new(api_url: String, client: FoxhuntClient) -> Self { + Self { api_url, client } } /// Run the server loop: read JSON lines from stdin, write responses to stdout. @@ -140,9 +145,6 @@ impl McpServer { } /// Handle `tools/call` -- execute a tool and return the result. - /// - /// Currently returns stub responses; real implementations will use - /// the same gRPC calls as the CLI commands. async fn handle_tools_call(&self, id: Value, params: &Value) -> JsonRpcResponse { let Some(tool_name) = params.get("name").and_then(Value::as_str) else { return error_response(id, INVALID_PARAMS, "missing 'name' in tools/call params"); @@ -164,7 +166,6 @@ impl McpServer { ); } - // Execute the tool (stub for now). let result = self.execute_tool(tool_name, &arguments).await; match result { @@ -195,178 +196,1407 @@ impl McpServer { /// Execute a tool by name with the given arguments. /// - /// Stub implementation: returns a placeholder message for each tool. - /// Real implementations will use gRPC calls to the API Gateway, - /// reusing the same client logic as the CLI commands. + /// Each tool maps to a real gRPC call against the API Gateway. async fn execute_tool( &self, tool_name: &str, arguments: &Value, ) -> Result { - // Dispatch by tool name. For now, return informative stubs that - // indicate what will eventually happen. - let response = match tool_name { + match tool_name { // ── Service ───────────────────────────────────────────── - "fxt_service_list" => "stub: would list all services via gRPC ServiceDiscovery".into(), - "fxt_service_status" => { - let svc = arg_str(arguments, "service").unwrap_or_default(); - format!("stub: would get status of service '{svc}' via gRPC") - } - "fxt_service_health" => { - "stub: would health-check all services via gRPC Health/Check".into() - } + "fxt_service_list" => self.tool_service_list().await, + "fxt_service_status" => self.tool_service_status(arguments).await, + "fxt_service_health" => self.tool_service_health().await, // ── Training ──────────────────────────────────────────── - "fxt_train_start" => { - let model = arg_str(arguments, "model").unwrap_or_default(); - let symbol = arg_str(arguments, "symbol") - .unwrap_or_else(|| "ES.FUT".into()); - format!("stub: would start training model={model} symbol={symbol} via gRPC MLTraining/StartTraining") - } - "fxt_train_stop" => { - let job_id = arg_str(arguments, "job_id").unwrap_or_default(); - format!("stub: would stop training job={job_id} via gRPC MLTraining/StopTraining") - } - "fxt_train_status" => { - let job_id = arg_str(arguments, "job_id") - .unwrap_or_else(|| "latest".into()); - format!( - "stub: would get training status job={job_id} via gRPC MLTraining/GetTrainingStatus" - ) - } - "fxt_train_list" => { - "stub: would list training jobs via gRPC MLTraining/ListTrainingJobs".into() - } + "fxt_train_start" => self.tool_train_start(arguments).await, + "fxt_train_stop" => self.tool_train_stop(arguments).await, + "fxt_train_status" => self.tool_train_status(arguments).await, + "fxt_train_list" => self.tool_train_list().await, // ── Trading ───────────────────────────────────────────── - "fxt_trade_positions" => { - "stub: would get positions via gRPC TradingService/GetPositions".into() - } - "fxt_trade_orders" => { - "stub: would list orders via gRPC TradingService/GetOrders".into() - } - "fxt_trade_account" => { - "stub: would get account state via gRPC TradingService/GetAccountState".into() - } - "fxt_trade_submit" => { - let symbol = arg_str(arguments, "symbol").unwrap_or_default(); - let side = arg_str(arguments, "side").unwrap_or_default(); - let qty = arguments - .get("quantity") - .and_then(Value::as_f64) - .unwrap_or(0.0); - let otype = arg_str(arguments, "order_type").unwrap_or_default(); - format!( - "stub: would submit order symbol={symbol} side={side} qty={qty} type={otype} via gRPC TradingService/SubmitOrder" - ) - } + "fxt_trade_positions" => self.tool_trade_positions().await, + "fxt_trade_orders" => self.tool_trade_orders(arguments).await, + "fxt_trade_account" => self.tool_trade_account().await, + "fxt_trade_submit" => self.tool_trade_submit(arguments).await, // ── Models ────────────────────────────────────────────── - "fxt_model_list" => "stub: would list models via gRPC MLService/ListModels".into(), - "fxt_model_status" => { - let model = arg_str(arguments, "model").unwrap_or_default(); - format!("stub: would get model status model={model} via gRPC MLService/GetModelStatus") - } - "fxt_model_predict" => { - let model = arg_str(arguments, "model").unwrap_or_default(); - let symbol = arg_str(arguments, "symbol").unwrap_or_default(); - format!( - "stub: would get prediction model={model} symbol={symbol} via gRPC MLService/Predict" - ) - } - "fxt_model_ensemble" => { - let symbol = arg_str(arguments, "symbol").unwrap_or_default(); - format!( - "stub: would get ensemble vote symbol={symbol} via gRPC MLService/GetEnsembleVote" - ) - } + "fxt_model_list" => self.tool_model_list().await, + "fxt_model_status" => self.tool_model_status(arguments).await, + "fxt_model_predict" => self.tool_model_predict(arguments).await, + "fxt_model_ensemble" => self.tool_model_ensemble(arguments).await, // ── Broker ────────────────────────────────────────────── - "fxt_broker_status" => { - "stub: would get broker status via gRPC BrokerGateway/GetSessionStatus".into() - } - "fxt_broker_connect" => { - "stub: would connect broker via gRPC BrokerGateway/Connect".into() - } + "fxt_broker_status" => self.tool_broker_status().await, + "fxt_broker_connect" => self.tool_broker_connect().await, // ── Risk ──────────────────────────────────────────────── - "fxt_risk_status" => { - "stub: would get risk status via gRPC RiskService/GetRiskStatus".into() - } - "fxt_risk_limits" => { - "stub: would get risk limits via gRPC RiskService/GetLimits".into() - } - "fxt_risk_drawdown" => { - "stub: would get drawdown stats via gRPC RiskService/GetDrawdown".into() - } - "fxt_risk_emergency" => { - let action = arg_str(arguments, "action").unwrap_or_default(); - format!( - "stub: would {action} trading via gRPC RiskService/EmergencyAction" - ) - } + "fxt_risk_status" => self.tool_risk_status().await, + "fxt_risk_limits" => self.tool_risk_limits().await, + "fxt_risk_drawdown" => self.tool_risk_drawdown().await, + "fxt_risk_emergency" => self.tool_risk_emergency(arguments).await, // ── Data ──────────────────────────────────────────────── - "fxt_data_status" => { - "stub: would get data status via gRPC DataAcquisition/GetStatus".into() - } - "fxt_data_feeds" => { - "stub: would list data feeds via gRPC DataAcquisition/ListFeeds".into() - } + "fxt_data_status" => self.tool_data_status().await, + "fxt_data_feeds" => self.tool_data_feeds().await, // ── Cluster ───────────────────────────────────────────── - "fxt_cluster_status" => { - "stub: would get cluster status via gRPC Monitoring/GetClusterStatus".into() - } - "fxt_cluster_resources" => { - "stub: would get resource utilization via gRPC Monitoring/GetResources".into() - } + "fxt_cluster_status" => self.tool_cluster_status().await, + "fxt_cluster_resources" => self.tool_cluster_resources().await, // ── Agent ─────────────────────────────────────────────── - "fxt_agent_start" => { - "stub: would start agent via gRPC TradingAgent/StartAgent".into() - } - "fxt_agent_stop" => { - "stub: would stop agent via gRPC TradingAgent/StopAgent".into() - } - "fxt_agent_status" => { - "stub: would get agent status via gRPC TradingAgent/GetAgentStatus".into() - } + "fxt_agent_start" => self.tool_agent_start().await, + "fxt_agent_stop" => self.tool_agent_stop().await, + "fxt_agent_status" => self.tool_agent_status().await, // ── Config ────────────────────────────────────────────── - "fxt_config_get" => { - let key = arg_str(arguments, "key").unwrap_or_default(); - format!("stub: would get config key={key} via gRPC ConfigService/GetConfig") - } - "fxt_config_set" => { - let key = arg_str(arguments, "key").unwrap_or_default(); - let val = arg_str(arguments, "value").unwrap_or_default(); - format!( - "stub: would set config key={key} value={val} via gRPC ConfigService/SetConfig" - ) - } + "fxt_config_get" => self.tool_config_get(arguments).await, + "fxt_config_set" => self.tool_config_set(arguments).await, // ── Tune ──────────────────────────────────────────────── - "fxt_tune_start" => { - let model = arg_str(arguments, "model").unwrap_or_default(); - format!( - "stub: would start tuning model={model} via gRPC MLTraining/StartHyperopt" - ) - } - "fxt_tune_status" => { - "stub: would get tuning progress via gRPC MLTraining/GetHyperoptStatus".into() - } + "fxt_tune_start" => self.tool_tune_start(arguments).await, + "fxt_tune_status" => self.tool_tune_status(arguments).await, - _ => return Err(anyhow::anyhow!("unknown tool: {tool_name}")), + _ => Err(anyhow::anyhow!("unknown tool: {tool_name}")), + } + } + + // ══════════════════════════════════════════════════════════════════ + // Argument helpers + // ══════════════════════════════════════════════════════════════════ + + /// Extract a required string argument. + fn arg_str<'args>(args: &'args Value, key: &str) -> Result<&'args str, anyhow::Error> { + args.get(key) + .and_then(Value::as_str) + .ok_or_else(|| anyhow::anyhow!("missing required argument: {key}")) + } + + /// Extract an optional string argument. + fn arg_str_opt<'args>(args: &'args Value, key: &str) -> Option<&'args str> { + args.get(key).and_then(Value::as_str) + } + + /// Extract an optional f64 argument. + fn arg_f64_opt(args: &Value, key: &str) -> Option { + args.get(key).and_then(Value::as_f64) + } + + // ══════════════════════════════════════════════════════════════════ + // Service tools + // ══════════════════════════════════════════════════════════════════ + + async fn tool_service_list(&self) -> Result { + let resp = self + .client + .monitoring() + .get_system_status(crate::proto::monitoring::GetSystemStatusRequest { + service_names: vec![], + }) + .await? + .into_inner(); + + let overall = resp.overall_status.unwrap_or_default(); + let services: Vec = resp + .service_statuses + .iter() + .map(|s| { + json!({ + "name": s.service_name, + "health": service_health_str(s.health), + "state": service_state_str(s.state), + "version": s.version.clone().unwrap_or_default(), + "uptime_seconds": s.uptime_seconds, + "error": s.error_message.clone().unwrap_or_default(), + }) + }) + .collect(); + + Ok(serde_json::to_string_pretty(&json!({ + "overall_health": system_health_str(overall.overall_health), + "healthy": overall.healthy_services, + "total": overall.total_services, + "uptime_seconds": overall.system_uptime_seconds, + "services": services, + }))?) + } + + async fn tool_service_status(&self, args: &Value) -> Result { + let service = Self::arg_str(args, "service")?; + let resp = self + .client + .monitoring() + .get_system_status(crate::proto::monitoring::GetSystemStatusRequest { + service_names: vec![service.to_owned()], + }) + .await? + .into_inner(); + + let svc = resp + .service_statuses + .into_iter() + .find(|s| s.service_name == service) + .ok_or_else(|| anyhow::anyhow!("service '{service}' not found"))?; + + let deps: Vec = svc + .dependencies + .iter() + .map(|d| { + json!({ + "name": d.name, + "status": health_status_str(d.status), + "endpoint": d.endpoint.clone().unwrap_or_default(), + "response_time_ms": d.response_time_ms.unwrap_or_default(), + }) + }) + .collect(); + + Ok(serde_json::to_string_pretty(&json!({ + "name": svc.service_name, + "health": service_health_str(svc.health), + "state": service_state_str(svc.state), + "version": svc.version.unwrap_or_default(), + "uptime_seconds": svc.uptime_seconds, + "error": svc.error_message.unwrap_or_default(), + "metadata": svc.metadata, + "dependencies": deps, + }))?) + } + + async fn tool_service_health(&self) -> Result { + let resp = self + .client + .monitoring() + .get_health_check(crate::proto::monitoring::GetHealthCheckRequest { + service_name: None, + }) + .await? + .into_inner(); + + let checks: Vec = resp + .health_checks + .iter() + .map(|c| { + json!({ + "name": c.check_name, + "status": health_status_str(c.status), + "message": c.message.clone().unwrap_or_default(), + "response_time_ms": c.response_time_ms.unwrap_or_default(), + }) + }) + .collect(); + + Ok(serde_json::to_string_pretty(&json!({ + "overall": health_status_str(resp.health_status), + "checks": checks, + }))?) + } + + // ══════════════════════════════════════════════════════════════════ + // Training tools + // ══════════════════════════════════════════════════════════════════ + + async fn tool_train_start(&self, args: &Value) -> Result { + let model = Self::arg_str(args, "model")?; + let symbol = Self::arg_str_opt(args, "symbol"); + + let data_source = symbol.map(|s| crate::proto::ml_training::DataSource { + source: Some(crate::proto::ml_training::data_source::Source::FilePath( + s.to_owned(), + )), + start_time: 0, + end_time: 0, + }); + + let resp = self + .client + .ml_training() + .start_training(crate::proto::ml_training::StartTrainingRequest { + model_type: model.to_uppercase(), + data_source, + hyperparameters: None, + use_gpu: true, + description: String::new(), + tags: Default::default(), + mode: crate::proto::ml_training::TrainingMode::Full as i32, + resume_checkpoint_path: String::new(), + max_epochs: 0, + }) + .await? + .into_inner(); + + Ok(serde_json::to_string_pretty(&json!({ + "job_id": resp.job_id, + "status": training_status_str(resp.status), + "message": resp.message, + }))?) + } + + async fn tool_train_stop(&self, args: &Value) -> Result { + let job_id = Self::arg_str(args, "job_id")?; + let resp = self + .client + .ml_training() + .stop_training(crate::proto::ml_training::StopTrainingRequest { + job_id: job_id.to_owned(), + reason: String::new(), + }) + .await? + .into_inner(); + + Ok(serde_json::to_string_pretty(&json!({ + "success": resp.success, + "message": resp.message, + }))?) + } + + async fn tool_train_status(&self, args: &Value) -> Result { + let job_id = Self::arg_str(args, "job_id")?; + let resp = self + .client + .ml_training() + .get_training_job_details( + crate::proto::ml_training::GetTrainingJobDetailsRequest { + job_id: job_id.to_owned(), + }, + ) + .await? + .into_inner(); + + let details = resp.job_details.unwrap_or_default(); + let fm = details.final_financial_metrics.as_ref(); + + Ok(serde_json::to_string_pretty(&json!({ + "job_id": details.job_id, + "model_type": details.model_type, + "status": training_status_str(details.status), + "description": details.description, + "created_at": details.created_at, + "started_at": details.started_at, + "completed_at": details.completed_at, + "error": details.error_message, + "artifact_path": details.model_artifact_path, + "financial_metrics": fm.map(|m| json!({ + "sharpe_ratio": m.sharpe_ratio, + "max_drawdown": m.max_drawdown, + "hit_rate": m.hit_rate, + "simulated_return": m.simulated_return, + })), + }))?) + } + + async fn tool_train_list(&self) -> Result { + let resp = self + .client + .ml_training() + .list_training_jobs(crate::proto::ml_training::ListTrainingJobsRequest { + page: 0, + page_size: 50, + status_filter: 0, + model_type_filter: String::new(), + start_time: 0, + end_time: 0, + }) + .await? + .into_inner(); + + let jobs: Vec = resp + .jobs + .iter() + .map(|j| { + json!({ + "job_id": j.job_id, + "model_type": j.model_type, + "status": training_status_str(j.status), + "description": j.description, + "final_loss": j.final_loss, + "best_validation_score": j.best_validation_score, + "created_at": j.created_at, + }) + }) + .collect(); + + Ok(serde_json::to_string_pretty(&json!({ + "total": resp.total_count, + "jobs": jobs, + }))?) + } + + // ══════════════════════════════════════════════════════════════════ + // Trading tools + // ══════════════════════════════════════════════════════════════════ + + async fn tool_trade_positions(&self) -> Result { + let resp = self + .client + .trading() + .get_positions(crate::proto::trading::GetPositionsRequest { + account_id: None, + symbol: None, + }) + .await? + .into_inner(); + + let positions: Vec = resp + .positions + .iter() + .map(|p| { + json!({ + "symbol": p.symbol, + "quantity": p.quantity, + "avg_price": p.average_price, + "market_value": p.market_value, + "unrealized_pnl": p.unrealized_pnl, + "realized_pnl": p.realized_pnl, + }) + }) + .collect(); + + Ok(serde_json::to_string_pretty(&json!({ + "positions": positions, + }))?) + } + + async fn tool_trade_orders(&self, args: &Value) -> Result { + let order_id = Self::arg_str_opt(args, "order_id"); + + if let Some(oid) = order_id { + let resp = self + .client + .trading() + .get_order_status(crate::proto::trading::GetOrderStatusRequest { + order_id: oid.to_owned(), + }) + .await? + .into_inner(); + + let order = resp + .order + .ok_or_else(|| anyhow::anyhow!("order {oid} not found"))?; + + Ok(serde_json::to_string_pretty(&json!({ + "order_id": order.order_id, + "symbol": order.symbol, + "side": side_name(order.side), + "quantity": order.quantity, + "filled_quantity": order.filled_quantity, + "order_type": order_type_name(order.order_type), + "price": order.price, + "status": order_status_name(order.status), + }))?) + } else { + // No order_id: return a hint + Ok(serde_json::to_string_pretty(&json!({ + "message": "Provide an order_id argument to look up a specific order. Use fxt_trade_positions for current positions.", + }))?) + } + } + + async fn tool_trade_account(&self) -> Result { + let resp = self + .client + .broker_gateway() + .get_account_state(crate::proto::broker_gateway::GetAccountStateRequest { + account_id: String::new(), + }) + .await? + .into_inner(); + + Ok(serde_json::to_string_pretty(&json!({ + "account_id": resp.account_id, + "cash_balance": resp.cash_balance, + "equity": resp.equity, + "margin_used": resp.margin_used, + "margin_available": resp.margin_available, + "buying_power": resp.buying_power, + "unrealized_pnl": resp.unrealized_pnl, + "realized_pnl": resp.realized_pnl, + }))?) + } + + async fn tool_trade_submit(&self, args: &Value) -> Result { + let symbol = Self::arg_str(args, "symbol")?; + let side = Self::arg_str(args, "side")?; + let quantity = Self::arg_f64_opt(args, "quantity") + .ok_or_else(|| anyhow::anyhow!("missing required argument: quantity"))?; + let order_type_str = Self::arg_str(args, "order_type")?; + let price = Self::arg_f64_opt(args, "price"); + + let side_val = match side.to_lowercase().as_str() { + "buy" | "long" => crate::proto::trading::OrderSide::Buy as i32, + "sell" | "short" => crate::proto::trading::OrderSide::Sell as i32, + _ => return Err(anyhow::anyhow!("invalid side '{side}': expected 'buy' or 'sell'")), }; - Ok(response) + let order_type = match order_type_str.to_lowercase().as_str() { + "market" => crate::proto::trading::OrderType::Market as i32, + "limit" => crate::proto::trading::OrderType::Limit as i32, + "stop" => crate::proto::trading::OrderType::Stop as i32, + _ => crate::proto::trading::OrderType::Market as i32, + }; + + let resp = self + .client + .trading() + .submit_order(crate::proto::trading::SubmitOrderRequest { + symbol: symbol.to_owned(), + side: side_val, + quantity, + order_type, + price, + stop_price: None, + account_id: String::new(), + metadata: std::collections::HashMap::new(), + }) + .await? + .into_inner(); + + Ok(serde_json::to_string_pretty(&json!({ + "order_id": resp.order_id, + "status": order_status_name(resp.status), + "message": resp.message, + }))?) + } + + // ══════════════════════════════════════════════════════════════════ + // Model tools + // ══════════════════════════════════════════════════════════════════ + + async fn tool_model_list(&self) -> Result { + let resp = self + .client + .ml() + .get_available_models(crate::proto::ml::GetAvailableModelsRequest {}) + .await? + .into_inner(); + + let models: Vec = resp + .available_models + .iter() + .map(|m| { + json!({ + "name": m.model_name, + "model_type": m.model_type, + "description": m.description, + "symbols": m.supported_symbols, + }) + }) + .collect(); + + Ok(serde_json::to_string_pretty(&json!({ + "count": models.len(), + "models": models, + }))?) + } + + async fn tool_model_status(&self, args: &Value) -> Result { + let model = Self::arg_str(args, "model")?; + let resp = self + .client + .ml() + .get_model_status(crate::proto::ml::GetModelStatusRequest { + model_name: Some(model.to_owned()), + }) + .await? + .into_inner(); + + let s = resp + .model_statuses + .into_iter() + .find(|s| s.model_name == model) + .ok_or_else(|| anyhow::anyhow!("model '{model}' not found"))?; + + Ok(serde_json::to_string_pretty(&json!({ + "name": s.model_name, + "state": model_state_str(s.state), + "health": model_health_str(s.health), + "error": s.error_message.unwrap_or_default(), + "last_updated": s.last_updated, + "last_prediction": s.last_prediction, + "metadata": s.metadata, + }))?) + } + + async fn tool_model_predict(&self, args: &Value) -> Result { + let model = Self::arg_str(args, "model")?; + let symbol = Self::arg_str(args, "symbol")?; + let resp = self + .client + .ml() + .get_prediction(crate::proto::ml::GetPredictionRequest { + model_name: model.to_owned(), + symbol: symbol.to_owned(), + horizon_minutes: None, + features: Default::default(), + }) + .await? + .into_inner(); + + let pred = resp.prediction.unwrap_or_default(); + + Ok(serde_json::to_string_pretty(&json!({ + "model": pred.model_name, + "symbol": pred.symbol, + "prediction_type": prediction_type_str(pred.prediction_type), + "value": pred.value, + "confidence": resp.confidence, + "horizon_minutes": pred.horizon_minutes, + }))?) + } + + async fn tool_model_ensemble(&self, args: &Value) -> Result { + let symbol = Self::arg_str(args, "symbol")?; + let resp = self + .client + .ml() + .get_ensemble_vote(crate::proto::ml::GetEnsembleVoteRequest { + symbol: symbol.to_owned(), + horizon_minutes: None, + model_names: vec![], + }) + .await? + .into_inner(); + + let vote = resp.ensemble_vote.unwrap_or_default(); + let individual: Vec = resp + .individual_votes + .iter() + .map(|v| { + json!({ + "model": v.model_name, + "prediction": prediction_type_str(v.prediction), + "confidence": v.confidence, + "weight": v.weight, + }) + }) + .collect(); + + Ok(serde_json::to_string_pretty(&json!({ + "symbol": vote.symbol, + "consensus": prediction_type_str(vote.consensus_prediction), + "confidence": resp.overall_confidence, + "signal_strength": signal_strength_str(vote.signal_strength), + "votes_buy": vote.votes_buy, + "votes_sell": vote.votes_sell, + "votes_hold": vote.votes_hold, + "total_models": vote.total_models, + "individual_votes": individual, + }))?) + } + + // ══════════════════════════════════════════════════════════════════ + // Broker tools + // ══════════════════════════════════════════════════════════════════ + + async fn tool_broker_status(&self) -> Result { + let resp = self + .client + .broker_gateway() + .get_session_status(crate::proto::broker_gateway::GetSessionStatusRequest { + session_id: None, + }) + .await? + .into_inner(); + + Ok(serde_json::to_string_pretty(&json!({ + "session_id": resp.session_id, + "state": session_state_name(resp.state), + "sender_seq_num": resp.sender_seq_num, + "target_seq_num": resp.target_seq_num, + "heartbeat_rtt_ms": resp.heartbeat_rtt_ms, + "details": resp.details, + }))?) + } + + async fn tool_broker_connect(&self) -> Result { + let resp = self + .client + .broker_gateway() + .health_check(crate::proto::broker_gateway::HealthCheckRequest {}) + .await? + .into_inner(); + + Ok(serde_json::to_string_pretty(&json!({ + "healthy": resp.healthy, + "message": resp.message, + "details": resp.details, + }))?) + } + + // ══════════════════════════════════════════════════════════════════ + // Risk tools + // ══════════════════════════════════════════════════════════════════ + + async fn tool_risk_status(&self) -> Result { + let metrics_resp = self + .client + .risk() + .get_risk_metrics(crate::proto::risk::GetRiskMetricsRequest { + portfolio_id: None, + }) + .await? + .into_inner(); + + let cb_resp = self + .client + .risk() + .get_circuit_breaker_status( + crate::proto::risk::GetCircuitBreakerStatusRequest { symbol: None }, + ) + .await? + .into_inner(); + + let m = metrics_resp.metrics.unwrap_or_default(); + + let breakers: Vec = cb_resp + .circuit_breakers + .iter() + .map(|cb| { + json!({ + "name": cb.name, + "is_triggered": cb.is_triggered, + "trigger_reason": cb.trigger_reason.clone().unwrap_or_default(), + "breaker_type": breaker_type_name(cb.breaker_type), + }) + }) + .collect(); + + Ok(serde_json::to_string_pretty(&json!({ + "var_1d": m.portfolio_var_1d, + "var_5d": m.portfolio_var_5d, + "current_drawdown": m.current_drawdown, + "max_drawdown": m.max_drawdown, + "sharpe_ratio": m.sharpe_ratio, + "sortino_ratio": m.sortino_ratio, + "volatility": m.volatility, + "circuit_breakers": breakers, + }))?) + } + + async fn tool_risk_limits(&self) -> Result { + let metrics_resp = self + .client + .risk() + .get_risk_metrics(crate::proto::risk::GetRiskMetricsRequest { + portfolio_id: None, + }) + .await? + .into_inner(); + + let m = metrics_resp.metrics.unwrap_or_default(); + + let positions: Vec = m + .position_risks + .iter() + .map(|p| { + json!({ + "symbol": p.symbol, + "position_size": p.position_size, + "market_value": p.market_value, + "var_contribution": p.var_contribution, + "concentration_risk": p.concentration_risk, + "liquidity_risk": p.liquidity_risk, + }) + }) + .collect(); + + Ok(serde_json::to_string_pretty(&json!({ + "var_1d": m.portfolio_var_1d, + "var_5d": m.portfolio_var_5d, + "var_30d": m.portfolio_var_30d, + "positions": positions, + }))?) + } + + async fn tool_risk_drawdown(&self) -> Result { + let metrics_resp = self + .client + .risk() + .get_risk_metrics(crate::proto::risk::GetRiskMetricsRequest { + portfolio_id: None, + }) + .await? + .into_inner(); + + let m = metrics_resp.metrics.unwrap_or_default(); + + Ok(serde_json::to_string_pretty(&json!({ + "current_drawdown": m.current_drawdown, + "max_drawdown": m.max_drawdown, + "sharpe_ratio": m.sharpe_ratio, + "sortino_ratio": m.sortino_ratio, + "beta": m.beta, + "alpha": m.alpha, + }))?) + } + + async fn tool_risk_emergency(&self, args: &Value) -> Result { + let action = Self::arg_str(args, "action")?; + + if action == "resume" { + // Emergency resume is not a standard gRPC call -- return guidance. + return Ok(serde_json::to_string_pretty(&json!({ + "message": "Resume from emergency halt requires manual operator action via the trading agent service.", + }))?); + } + + let resp = self + .client + .risk() + .emergency_stop(crate::proto::risk::EmergencyStopRequest { + stop_type: crate::proto::risk::EmergencyStopType::AllTrading as i32, + reason: "Emergency halt via MCP tool".to_owned(), + symbol: None, + account_id: None, + }) + .await? + .into_inner(); + + Ok(serde_json::to_string_pretty(&json!({ + "success": resp.success, + "message": resp.message, + "affected_orders": resp.affected_orders, + }))?) + } + + // ══════════════════════════════════════════════════════════════════ + // Data tools + // ══════════════════════════════════════════════════════════════════ + + async fn tool_data_status(&self) -> Result { + let resp = self + .client + .data_acquisition() + .list_download_jobs( + crate::proto::data_acquisition::ListDownloadJobsRequest { + page: 0, + page_size: 50, + status_filter: 0, + start_time: 0, + end_time: 0, + }, + ) + .await? + .into_inner(); + + let jobs: Vec = resp + .jobs + .iter() + .map(|j| { + json!({ + "job_id": j.job_id, + "status": download_status_name(j.status), + "dataset": j.dataset, + "symbols": j.symbols, + "start_date": j.start_date, + "end_date": j.end_date, + "progress_pct": j.progress_percentage, + }) + }) + .collect(); + + Ok(serde_json::to_string_pretty(&json!({ + "total": resp.total_count, + "jobs": jobs, + }))?) + } + + async fn tool_data_feeds(&self) -> Result { + // The data acquisition service does not have a dedicated "list feeds" RPC. + // Return status from the service discovery / monitoring instead. + let resp = self + .client + .monitoring() + .get_system_status(crate::proto::monitoring::GetSystemStatusRequest { + service_names: vec!["data-acquisition".to_owned()], + }) + .await? + .into_inner(); + + let svc = resp + .service_statuses + .first() + .map(|s| { + json!({ + "service": s.service_name, + "health": service_health_str(s.health), + "state": service_state_str(s.state), + }) + }) + .unwrap_or(json!({"message": "data-acquisition service not found"})); + + Ok(serde_json::to_string_pretty(&svc)?) + } + + // ══════════════════════════════════════════════════════════════════ + // Cluster tools + // ══════════════════════════════════════════════════════════════════ + + async fn tool_cluster_status(&self) -> Result { + let resp = self + .client + .monitoring() + .get_system_status(crate::proto::monitoring::GetSystemStatusRequest { + service_names: vec![], + }) + .await? + .into_inner(); + + let overall = resp.overall_status.unwrap_or_default(); + let sys_metrics = overall.system_metrics.unwrap_or_default(); + + let services: Vec = resp + .service_statuses + .iter() + .map(|s| { + json!({ + "name": s.service_name, + "health": service_health_str(s.health), + "state": service_state_str(s.state), + "version": s.version.clone().unwrap_or_default(), + "uptime_seconds": s.uptime_seconds, + "error": s.error_message.clone().unwrap_or_default(), + }) + }) + .collect(); + + Ok(serde_json::to_string_pretty(&json!({ + "overall_health": system_health_str(overall.overall_health), + "healthy_services": overall.healthy_services, + "total_services": overall.total_services, + "critical_issues": overall.critical_issues, + "system_uptime_seconds": overall.system_uptime_seconds, + "cpu_usage_pct": sys_metrics.cpu_usage_percent, + "memory_usage_pct": sys_metrics.memory_usage_percent, + "disk_usage_pct": sys_metrics.disk_usage_percent, + "error_rate_pct": sys_metrics.error_rate_percent, + "services": services, + }))?) + } + + async fn tool_cluster_resources(&self) -> Result { + let resource_names = vec![ + "cpu.usage".to_owned(), + "memory.usage".to_owned(), + "disk.usage".to_owned(), + "gpu.utilization".to_owned(), + "gpu.memory.used".to_owned(), + "network.io".to_owned(), + ]; + + let resp = self + .client + .monitoring() + .get_metrics(crate::proto::monitoring::GetMetricsRequest { + metric_names: resource_names, + start_time: None, + end_time: None, + aggregation: None, + }) + .await? + .into_inner(); + + let metrics: Vec = resp + .metrics + .iter() + .map(|m| { + json!({ + "name": m.name, + "value": m.value, + "unit": m.unit, + }) + }) + .collect(); + + Ok(serde_json::to_string_pretty(&json!({ + "metrics": metrics, + }))?) + } + + // ══════════════════════════════════════════════════════════════════ + // Agent tools + // ══════════════════════════════════════════════════════════════════ + + async fn tool_agent_start(&self) -> Result { + // List all strategies and enable each one. + let list_resp = self + .client + .trading_agent() + .list_strategies(crate::proto::trading_agent::ListStrategiesRequest { + status_filter: None, + }) + .await? + .into_inner(); + + let mut results = Vec::new(); + for s in &list_resp.strategies { + let resp = self + .client + .trading_agent() + .update_strategy_status( + crate::proto::trading_agent::UpdateStrategyStatusRequest { + strategy_id: s.strategy_id.clone(), + new_status: crate::proto::trading_agent::StrategyStatus::Enabled as i32, + reason: None, + }, + ) + .await? + .into_inner(); + + results.push(json!({ + "strategy_id": s.strategy_id, + "success": resp.success, + "message": resp.message, + })); + } + + Ok(serde_json::to_string_pretty(&json!({ + "action": "start", + "strategies_updated": results.len(), + "results": results, + }))?) + } + + async fn tool_agent_stop(&self) -> Result { + // List all strategies and disable each one. + let list_resp = self + .client + .trading_agent() + .list_strategies(crate::proto::trading_agent::ListStrategiesRequest { + status_filter: None, + }) + .await? + .into_inner(); + + let mut results = Vec::new(); + for s in &list_resp.strategies { + let resp = self + .client + .trading_agent() + .update_strategy_status( + crate::proto::trading_agent::UpdateStrategyStatusRequest { + strategy_id: s.strategy_id.clone(), + new_status: crate::proto::trading_agent::StrategyStatus::Disabled as i32, + reason: None, + }, + ) + .await? + .into_inner(); + + results.push(json!({ + "strategy_id": s.strategy_id, + "success": resp.success, + "message": resp.message, + })); + } + + Ok(serde_json::to_string_pretty(&json!({ + "action": "stop", + "strategies_updated": results.len(), + "results": results, + }))?) + } + + async fn tool_agent_status(&self) -> Result { + let resp = self + .client + .trading_agent() + .get_agent_status(crate::proto::trading_agent::GetAgentStatusRequest { + include_performance: true, + include_positions: false, + }) + .await? + .into_inner(); + + let status = resp.status.unwrap_or_default(); + let perf = resp.performance.as_ref(); + + Ok(serde_json::to_string_pretty(&json!({ + "state": agent_state_name(status.state), + "active_strategies": status.active_strategies, + "selected_assets": status.selected_assets, + "portfolio_utilization": status.portfolio_utilization, + "current_universe_id": status.current_universe_id, + "performance": perf.map(|p| json!({ + "total_pnl": p.total_pnl, + "sharpe_ratio": p.sharpe_ratio, + "max_drawdown": p.max_drawdown, + "win_rate": p.win_rate, + "total_trades": p.total_trades, + "avg_trade_pnl": p.avg_trade_pnl, + })), + }))?) + } + + // ══════════════════════════════════════════════════════════════════ + // Config tools + // ══════════════════════════════════════════════════════════════════ + + async fn tool_config_get(&self, args: &Value) -> Result { + let key = Self::arg_str(args, "key")?; + let resp = self + .client + .config() + .get_configuration(crate::proto::config::GetConfigurationRequest { + category: None, + key: Some(key.to_owned()), + environment: None, + }) + .await? + .into_inner(); + + let settings: Vec = resp + .settings + .iter() + .map(|s| { + json!({ + "category": s.category, + "key": s.key, + "value": if s.sensitive { "********" } else { &s.value }, + "data_type": config_data_type_name(s.data_type), + "hot_reload": s.hot_reload, + "description": s.description, + }) + }) + .collect(); + + Ok(serde_json::to_string_pretty(&json!({ + "settings": settings, + }))?) + } + + async fn tool_config_set(&self, args: &Value) -> Result { + let key = Self::arg_str(args, "key")?; + let value = Self::arg_str(args, "value")?; + let resp = self + .client + .config() + .update_configuration(crate::proto::config::UpdateConfigurationRequest { + category: "default".to_owned(), + key: key.to_owned(), + value: value.to_owned(), + changed_by: "fxt-mcp".to_owned(), + change_reason: None, + environment: None, + }) + .await? + .into_inner(); + + Ok(serde_json::to_string_pretty(&json!({ + "success": resp.success, + "message": resp.message, + }))?) + } + + // ══════════════════════════════════════════════════════════════════ + // Tune tools + // ══════════════════════════════════════════════════════════════════ + + async fn tool_tune_start(&self, args: &Value) -> Result { + let model = Self::arg_str(args, "model")?; + let resp = self + .client + .ml_training() + .start_tuning_job(crate::proto::ml_training::StartTuningJobRequest { + model_type: model.to_uppercase(), + num_trials: 50, + config_path: String::new(), + data_source: None, + use_gpu: true, + description: String::new(), + tags: Default::default(), + }) + .await? + .into_inner(); + + Ok(serde_json::to_string_pretty(&json!({ + "job_id": resp.job_id, + "status": tuning_status_str(resp.status), + "message": resp.message, + }))?) + } + + async fn tool_tune_status(&self, args: &Value) -> Result { + let job_id = Self::arg_str_opt(args, "job_id") + .ok_or_else(|| anyhow::anyhow!("missing required argument: job_id (provide the tuning job ID)"))?; + + let resp = self + .client + .ml_training() + .get_tuning_job_status(crate::proto::ml_training::GetTuningJobStatusRequest { + job_id: job_id.to_owned(), + }) + .await? + .into_inner(); + + let best_params: Vec = resp + .best_params + .iter() + .map(|(k, v)| json!({"name": k, "value": v})) + .collect(); + + let best_metrics: Vec = resp + .best_metrics + .iter() + .map(|(k, v)| json!({"name": k, "value": v})) + .collect(); + + let trials: Vec = resp + .trial_history + .iter() + .map(|t| { + json!({ + "trial": t.trial_number, + "objective": t.objective_value, + "state": trial_state_str(t.state), + "started_at": t.started_at, + "completed_at": t.completed_at, + }) + }) + .collect(); + + Ok(serde_json::to_string_pretty(&json!({ + "job_id": resp.job_id, + "status": tuning_status_str(resp.status), + "current_trial": resp.current_trial, + "total_trials": resp.total_trials, + "best_params": best_params, + "best_metrics": best_metrics, + "message": resp.message, + "started_at": resp.started_at, + "updated_at": resp.updated_at, + "trials": trials, + }))?) } } -/// Extract a string argument from the arguments object. -fn arg_str(args: &Value, key: &str) -> Option { - args.get(key).and_then(Value::as_str).map(String::from) +// ══════════════════════════════════════════════════════════════════════ +// Enum display helpers (match the CLI command patterns) +// ══════════════════════════════════════════════════════════════════════ + +fn service_health_str(h: i32) -> &'static str { + use crate::proto::monitoring::ServiceHealth; + match ServiceHealth::try_from(h) { + Ok(ServiceHealth::Healthy) => "healthy", + Ok(ServiceHealth::Degraded) => "degraded", + Ok(ServiceHealth::Unhealthy) => "unhealthy", + Ok(ServiceHealth::Critical) => "critical", + _ => "unknown", + } +} + +fn service_state_str(s: i32) -> &'static str { + use crate::proto::monitoring::ServiceState; + match ServiceState::try_from(s) { + Ok(ServiceState::Starting) => "starting", + Ok(ServiceState::Running) => "running", + Ok(ServiceState::Stopping) => "stopping", + Ok(ServiceState::Stopped) => "stopped", + Ok(ServiceState::Error) => "error", + _ => "unknown", + } +} + +fn system_health_str(h: i32) -> &'static str { + use crate::proto::monitoring::SystemHealth; + match SystemHealth::try_from(h) { + Ok(SystemHealth::Healthy) => "healthy", + Ok(SystemHealth::Degraded) => "degraded", + Ok(SystemHealth::Unhealthy) => "unhealthy", + Ok(SystemHealth::Critical) => "critical", + _ => "unknown", + } +} + +fn health_status_str(h: i32) -> &'static str { + use crate::proto::monitoring::HealthStatus; + match HealthStatus::try_from(h) { + Ok(HealthStatus::Healthy) => "healthy", + Ok(HealthStatus::Degraded) => "degraded", + Ok(HealthStatus::Unhealthy) => "unhealthy", + Ok(HealthStatus::Critical) => "critical", + _ => "unknown", + } +} + +fn training_status_str(s: i32) -> &'static str { + use crate::proto::ml_training::TrainingStatus; + match TrainingStatus::try_from(s) { + Ok(TrainingStatus::Pending) => "pending", + Ok(TrainingStatus::Running) => "running", + Ok(TrainingStatus::Completed) => "completed", + Ok(TrainingStatus::Failed) => "failed", + Ok(TrainingStatus::Stopped) => "stopped", + Ok(TrainingStatus::Paused) => "paused", + _ => "unknown", + } +} + +fn tuning_status_str(s: i32) -> &'static str { + use crate::proto::ml_training::TuningJobStatus; + match TuningJobStatus::try_from(s) { + Ok(TuningJobStatus::TuningPending) => "pending", + Ok(TuningJobStatus::TuningRunning) => "running", + Ok(TuningJobStatus::TuningCompleted) => "completed", + Ok(TuningJobStatus::TuningFailed) => "failed", + Ok(TuningJobStatus::TuningStopped) => "stopped", + _ => "unknown", + } +} + +fn trial_state_str(s: i32) -> &'static str { + use crate::proto::ml_training::TrialState; + match TrialState::try_from(s) { + Ok(TrialState::TrialRunning) => "running", + Ok(TrialState::TrialComplete) => "complete", + Ok(TrialState::TrialPruned) => "pruned", + Ok(TrialState::TrialFailed) => "failed", + _ => "unknown", + } +} + +fn side_name(v: i32) -> &'static str { + use crate::proto::trading::OrderSide; + match OrderSide::try_from(v) { + Ok(OrderSide::Buy) => "BUY", + Ok(OrderSide::Sell) => "SELL", + _ => "UNKNOWN", + } +} + +fn order_type_name(v: i32) -> &'static str { + use crate::proto::trading::OrderType; + match OrderType::try_from(v) { + Ok(OrderType::Market) => "MARKET", + Ok(OrderType::Limit) => "LIMIT", + Ok(OrderType::Stop) => "STOP", + Ok(OrderType::StopLimit) => "STOP_LIMIT", + _ => "UNKNOWN", + } +} + +fn order_status_name(v: i32) -> &'static str { + use crate::proto::trading::OrderStatus; + match OrderStatus::try_from(v) { + Ok(OrderStatus::Pending) => "PENDING", + Ok(OrderStatus::Submitted) => "SUBMITTED", + Ok(OrderStatus::PartiallyFilled) => "PARTIALLY_FILLED", + Ok(OrderStatus::Filled) => "FILLED", + Ok(OrderStatus::Cancelled) => "CANCELLED", + Ok(OrderStatus::Rejected) => "REJECTED", + _ => "UNKNOWN", + } +} + +fn model_state_str(s: i32) -> &'static str { + use crate::proto::ml::ModelState; + match ModelState::try_from(s) { + Ok(ModelState::Loading) => "loading", + Ok(ModelState::Ready) => "ready", + Ok(ModelState::Predicting) => "predicting", + Ok(ModelState::Training) => "training", + Ok(ModelState::Error) => "error", + Ok(ModelState::Offline) => "offline", + _ => "unknown", + } +} + +fn model_health_str(h: i32) -> &'static str { + use crate::proto::ml::ModelHealth; + match ModelHealth::try_from(h) { + Ok(ModelHealth::Healthy) => "healthy", + Ok(ModelHealth::Degraded) => "degraded", + Ok(ModelHealth::Unhealthy) => "unhealthy", + Ok(ModelHealth::Critical) => "critical", + _ => "unknown", + } +} + +fn prediction_type_str(p: i32) -> &'static str { + use crate::proto::ml::PredictionType; + match PredictionType::try_from(p) { + Ok(PredictionType::Buy) => "BUY", + Ok(PredictionType::Sell) => "SELL", + Ok(PredictionType::Hold) => "HOLD", + Ok(PredictionType::PriceUp) => "PRICE_UP", + Ok(PredictionType::PriceDown) => "PRICE_DOWN", + Ok(PredictionType::VolatilityHigh) => "VOL_HIGH", + Ok(PredictionType::VolatilityLow) => "VOL_LOW", + _ => "UNKNOWN", + } +} + +fn signal_strength_str(s: i32) -> &'static str { + use crate::proto::ml::SignalStrength; + match SignalStrength::try_from(s) { + Ok(SignalStrength::VeryWeak) => "very_weak", + Ok(SignalStrength::Weak) => "weak", + Ok(SignalStrength::Moderate) => "moderate", + Ok(SignalStrength::Strong) => "strong", + Ok(SignalStrength::VeryStrong) => "very_strong", + _ => "unknown", + } +} + +fn session_state_name(v: i32) -> &'static str { + use crate::proto::broker_gateway::SessionState; + match SessionState::try_from(v) { + Ok(SessionState::Disconnected) => "DISCONNECTED", + Ok(SessionState::Connected) => "CONNECTED", + Ok(SessionState::LoggingIn) => "LOGGING_IN", + Ok(SessionState::Active) => "ACTIVE", + Ok(SessionState::LoggingOut) => "LOGGING_OUT", + _ => "UNKNOWN", + } +} + +fn breaker_type_name(value: i32) -> &'static str { + use crate::proto::risk::CircuitBreakerType; + match CircuitBreakerType::try_from(value) { + Ok(CircuitBreakerType::PortfolioLoss) => "portfolio-loss", + Ok(CircuitBreakerType::SymbolVolatility) => "symbol-volatility", + Ok(CircuitBreakerType::PositionSize) => "position-size", + Ok(CircuitBreakerType::Drawdown) => "drawdown", + _ => "unknown", + } +} + +fn agent_state_name(v: i32) -> &'static str { + use crate::proto::trading_agent::AgentState; + match AgentState::try_from(v) { + Ok(AgentState::Initializing) => "INITIALIZING", + Ok(AgentState::Active) => "ACTIVE", + Ok(AgentState::Paused) => "PAUSED", + Ok(AgentState::Error) => "ERROR", + Ok(AgentState::Shutdown) => "SHUTDOWN", + _ => "UNKNOWN", + } +} + +fn config_data_type_name(value: i32) -> &'static str { + use crate::proto::config::ConfigDataType; + match ConfigDataType::try_from(value) { + Ok(ConfigDataType::String) => "string", + Ok(ConfigDataType::Number) => "number", + Ok(ConfigDataType::Boolean) => "boolean", + Ok(ConfigDataType::Json) => "json", + Ok(ConfigDataType::Encrypted) => "encrypted", + _ => "unspecified", + } +} + +fn download_status_name(value: i32) -> &'static str { + use crate::proto::data_acquisition::DownloadStatus; + match DownloadStatus::try_from(value) { + Ok(DownloadStatus::Pending) => "PENDING", + Ok(DownloadStatus::Downloading) => "DOWNLOADING", + Ok(DownloadStatus::Validating) => "VALIDATING", + Ok(DownloadStatus::Uploading) => "UPLOADING", + Ok(DownloadStatus::Completed) => "COMPLETED", + Ok(DownloadStatus::Failed) => "FAILED", + Ok(DownloadStatus::Cancelled) => "CANCELLED", + _ => "UNKNOWN", + } } /// Write a JSON-RPC response as a single line to stdout. @@ -386,8 +1616,22 @@ async fn write_response( mod tests { use super::*; - fn make_server() -> McpServer { - McpServer::new("http://localhost:9090".into()) + /// Create an MCP server with a lazily-connected gRPC client (no real server). + /// Tests only exercise protocol handling; actual gRPC calls will return + /// transport errors which are surfaced as `isError` tool results. + async fn make_server() -> McpServer { + // Ensure the token storage directory exists so FileTokenStorage::new() + // does not fail during tests. + let config_dir = dirs::config_dir() + .unwrap_or_else(|| std::path::PathBuf::from("/tmp")) + .join("foxhunt-tli") + .join("tokens"); + let _ = std::fs::create_dir_all(&config_dir); + + let client = FoxhuntClient::connect("http://localhost:9090") + .await + .unwrap(); + McpServer::new("http://localhost:9090".into(), client) } fn make_request(method: &str, id: Option, params: Value) -> JsonRpcRequest { @@ -401,7 +1645,7 @@ mod tests { #[tokio::test] async fn initialize_returns_capabilities() { - let server = make_server(); + let server = make_server().await; let req = make_request("initialize", Some(json!(1)), json!({})); let resp = server.handle_request(&req).await.unwrap(); let result = resp.result.unwrap(); @@ -412,7 +1656,7 @@ mod tests { #[tokio::test] async fn notification_returns_none() { - let server = make_server(); + let server = make_server().await; let req = make_request("notifications/initialized", None, json!({})); let resp = server.handle_request(&req).await; assert!(resp.is_none()); @@ -420,7 +1664,7 @@ mod tests { #[tokio::test] async fn tools_list_returns_all_tools() { - let server = make_server(); + let server = make_server().await; let req = make_request("tools/list", Some(json!(2)), json!({})); let resp = server.handle_request(&req).await.unwrap(); let result = resp.result.unwrap(); @@ -435,24 +1679,9 @@ mod tests { } } - #[tokio::test] - async fn tools_call_known_tool() { - let server = make_server(); - let req = make_request( - "tools/call", - Some(json!(3)), - json!({"name": "fxt_service_list", "arguments": {}}), - ); - let resp = server.handle_request(&req).await.unwrap(); - assert!(resp.error.is_none()); - let result = resp.result.unwrap(); - let text = result["content"][0]["text"].as_str().unwrap(); - assert!(text.contains("stub")); - } - #[tokio::test] async fn tools_call_unknown_tool() { - let server = make_server(); + let server = make_server().await; let req = make_request( "tools/call", Some(json!(4)), @@ -465,7 +1694,7 @@ mod tests { #[tokio::test] async fn tools_call_missing_name() { - let server = make_server(); + let server = make_server().await; let req = make_request("tools/call", Some(json!(5)), json!({"arguments": {}})); let resp = server.handle_request(&req).await.unwrap(); assert!(resp.error.is_some()); @@ -474,57 +1703,36 @@ mod tests { #[tokio::test] async fn unknown_method_returns_error() { - let server = make_server(); + let server = make_server().await; let req = make_request("bogus/method", Some(json!(6)), json!({})); let resp = server.handle_request(&req).await.unwrap(); assert!(resp.error.is_some()); assert_eq!(resp.error.unwrap().code, METHOD_NOT_FOUND); } - #[tokio::test] - async fn tools_call_with_arguments() { - let server = make_server(); + #[tokio::test(flavor = "multi_thread")] + async fn tools_call_known_tool_returns_grpc_error() { + // With no real server, the gRPC call should fail with a transport error, + // which is returned as an isError tool result (not a protocol error). + // Requires multi_thread because the auth interceptor uses spawn_blocking. + let server = make_server().await; let req = make_request( "tools/call", - Some(json!(7)), - json!({ - "name": "fxt_train_start", - "arguments": {"model": "dqn", "symbol": "NQ.FUT"} - }), + Some(json!(3)), + json!({"name": "fxt_service_list", "arguments": {}}), ); let resp = server.handle_request(&req).await.unwrap(); + // Protocol-level success (no error field), but tool returned isError. + assert!(resp.error.is_none()); let result = resp.result.unwrap(); + assert_eq!(result["isError"], true); let text = result["content"][0]["text"].as_str().unwrap(); - assert!(text.contains("dqn")); - assert!(text.contains("NQ.FUT")); - } - - #[tokio::test] - async fn tools_call_trade_submit() { - let server = make_server(); - let req = make_request( - "tools/call", - Some(json!(8)), - json!({ - "name": "fxt_trade_submit", - "arguments": { - "symbol": "ES.FUT", - "side": "buy", - "quantity": 1, - "order_type": "market" - } - }), - ); - let resp = server.handle_request(&req).await.unwrap(); - let result = resp.result.unwrap(); - let text = result["content"][0]["text"].as_str().unwrap(); - assert!(text.contains("ES.FUT")); - assert!(text.contains("buy")); + assert!(text.contains("error:")); } #[tokio::test] async fn initialize_response_has_version() { - let server = make_server(); + let server = make_server().await; let req = make_request("initialize", Some(json!(9)), json!({})); let resp = server.handle_request(&req).await.unwrap(); let result = resp.result.unwrap(); @@ -532,23 +1740,6 @@ mod tests { assert!(!version.is_empty()); } - #[tokio::test] - async fn tool_execution_risk_emergency() { - let server = make_server(); - let req = make_request( - "tools/call", - Some(json!(10)), - json!({ - "name": "fxt_risk_emergency", - "arguments": {"action": "halt"} - }), - ); - let resp = server.handle_request(&req).await.unwrap(); - let result = resp.result.unwrap(); - let text = result["content"][0]["text"].as_str().unwrap(); - assert!(text.contains("halt")); - } - #[tokio::test] async fn malformed_json_returns_parse_error() { // We cannot drive the full `run()` loop easily, so test the parse @@ -562,7 +1753,7 @@ mod tests { #[tokio::test] async fn wrong_jsonrpc_version_returns_invalid_request() { - let server = make_server(); + let server = make_server().await; let req = JsonRpcRequest { jsonrpc: "1.0".into(), id: Some(json!(99)), diff --git a/bin/fxt/src/tui/cockpits/services.rs b/bin/fxt/src/tui/cockpits/services.rs index 0fb1b5132..56554f0d2 100644 --- a/bin/fxt/src/tui/cockpits/services.rs +++ b/bin/fxt/src/tui/cockpits/services.rs @@ -44,7 +44,17 @@ fn render_service_grid(frame: &mut Frame, area: Rect, state: &AppState) { .services .iter() .map(|s| { - let status_text = if s.healthy { "HEALTHY" } else { "DOWN" }; + let status_text = if s.healthy { + "HEALTHY".to_owned() + } else { + match &s.error { + Some(msg) if !msg.is_empty() => { + let truncated: String = msg.chars().take(20).collect(); + format!("DOWN: {truncated}") + } + _ => "DOWN".to_owned(), + } + }; let status_style = theme::status_style(s.healthy); let latency_style = if s.latency_ms > 500.0 { @@ -63,7 +73,7 @@ fn render_service_grid(frame: &mut Frame, area: Rect, state: &AppState) { Row::new(vec![ Span::styled(s.name.clone(), Style::default().fg(theme::TEXT)), - Span::styled((*status_text).to_owned(), status_style), + Span::styled(status_text, status_style), Span::styled(latency_text, latency_style), Span::styled(s.uptime.clone(), theme::muted_style()), Span::styled(s.version.clone(), theme::muted_style()), @@ -75,7 +85,7 @@ fn render_service_grid(frame: &mut Frame, area: Rect, state: &AppState) { rows, [ Constraint::Length(20), - Constraint::Length(10), + Constraint::Length(28), Constraint::Length(10), Constraint::Length(10), Constraint::Length(10), diff --git a/bin/fxt/src/tui/cockpits/training.rs b/bin/fxt/src/tui/cockpits/training.rs index fb52446b5..a3d8a6db8 100644 --- a/bin/fxt/src/tui/cockpits/training.rs +++ b/bin/fxt/src/tui/cockpits/training.rs @@ -51,7 +51,7 @@ fn render_sessions_table(frame: &mut Frame, area: Rect, state: &AppState) { Row::new(vec![ s.model.clone(), format!("{}", s.fold), - format!("{}/{}", s.epoch, s.max_epochs), + s.epoch.to_string(), format!("{:.4}", s.loss), format!("{:.4}", s.val_loss), format!("{:.1}", s.batches_per_sec), @@ -61,6 +61,11 @@ fn render_sessions_table(frame: &mut Frame, area: Rect, state: &AppState) { }) .collect(); + let block_title = if state.active_jobs > 0 { + format!(" Active Training Sessions -- {} K8s jobs ", state.active_jobs) + } else { + " Active Training Sessions ".to_owned() + }; let table = Table::new( rows, [ @@ -74,7 +79,7 @@ fn render_sessions_table(frame: &mut Frame, area: Rect, state: &AppState) { ], ) .header(header) - .block(theme::block(" Active Training Sessions ")); + .block(theme::block(&block_title)); frame.render_widget(table, area); } @@ -139,16 +144,18 @@ fn render_gpu_panel(frame: &mut Frame, area: Rect, state: &AppState) { theme::SUCCESS }; + let power_text = if state.gpu.power_limit_watts > 0 { + format!("{}W / {}W", state.gpu.power_watts, state.gpu.power_limit_watts) + } else { + format!("{}W", state.gpu.power_watts) + }; let info_items = vec![ListItem::new(Line::from(vec![ Span::styled( format!("{}C", state.gpu.temperature_c), Style::default().fg(temp_color), ), Span::styled(" ", theme::muted_style()), - Span::styled( - format!("{}W / {}W", state.gpu.power_watts, state.gpu.power_limit_watts), - theme::muted_style(), - ), + Span::styled(power_text, theme::muted_style()), ]))]; let info = List::new(info_items); frame.render_widget(info, chunks[3]); diff --git a/bin/fxt/src/tui/data_fetcher.rs b/bin/fxt/src/tui/data_fetcher.rs index 61b9dffc0..3e426a0d4 100644 --- a/bin/fxt/src/tui/data_fetcher.rs +++ b/bin/fxt/src/tui/data_fetcher.rs @@ -43,6 +43,7 @@ pub enum StateUpdate { sessions: Vec, gpu: GpuData, resources: SystemResources, + active_jobs: u32, }, /// Service health statuses (from GetSystemStatus polling). Services(Vec), @@ -170,7 +171,8 @@ impl DataFetcher { ram_total_gb: f64::from(m.memory_total_mb) / 1024.0, gpu_cluster_utilization: gpu.utilization, }; - let _ = tx.send(StateUpdate::TrainingMetrics { sessions, gpu, resources }); + let active_jobs = m.active_k8s_jobs; + let _ = tx.send(StateUpdate::TrainingMetrics { sessions, gpu, resources, active_jobs }); } Ok(None) => { debug!("TrainingMetrics stream ended"); @@ -211,6 +213,8 @@ impl DataFetcher { /// Service statuses — poll GetSystemStatus every 5s (the stream event /// only carries SystemMetrics, not individual ServiceStatus entries). + /// After 3 consecutive failures, reports a connection error to indicate + /// stale data in the services cockpit. fn spawn_system_status(&self, client: &FoxhuntClient) { let mut mon = client.monitoring(); let tx = self.tx.clone(); @@ -219,6 +223,7 @@ impl DataFetcher { tokio::spawn(async move { let mut interval = tokio::time::interval(Duration::from_secs(5)); + let mut consecutive_failures: u32 = 0; loop { tokio::select! { _ = cancel.cancelled() => return, @@ -230,6 +235,7 @@ impl DataFetcher { }); match mon.get_system_status(req).await { Ok(resp) => { + consecutive_failures = 0; if !connected_sent.swap(true, Ordering::Relaxed) { let _ = tx.send(StateUpdate::Connection(ConnectionStatus::Connected)); } @@ -242,7 +248,14 @@ impl DataFetcher { let _ = tx.send(StateUpdate::Services(services)); } Err(e) => { - debug!("SystemStatus poll failed: {e}"); + consecutive_failures = consecutive_failures.saturating_add(1); + debug!("SystemStatus poll failed ({consecutive_failures}/3): {e}"); + if consecutive_failures >= 3 { + warn!("SystemStatus: {consecutive_failures} consecutive failures, reporting stale"); + let _ = tx.send(StateUpdate::Connection(ConnectionStatus::Error( + format!("SystemStatus poll failed {consecutive_failures} times: {e}"), + ))); + } } } } @@ -989,6 +1002,7 @@ fn convert_service_status(s: &monitoring::ServiceStatus) -> ServiceData { uptime, version: s.version.clone().unwrap_or_default(), latency_ms, + error: s.error_message.clone(), } } @@ -1191,8 +1205,8 @@ fn convert_download_job(j: &data_acquisition::DownloadJobSummary) -> DataFeedDat DataFeedData { symbol: j.symbols.join(","), - records_per_sec: 0, // not in summary - latency_us: 0, + records_per_sec: 0, // Not available in proto DownloadJobSummary + latency_us: 0, // Not available in proto DownloadJobSummary status: status.to_owned(), } } @@ -1223,12 +1237,12 @@ fn group_pods(pods: Vec) -> Vec { groups .into_iter() - .map(|(service, pods)| { - let ready_count = pods.iter().filter(|p| p.ready).count(); - let total_count = pods.len(); + .map(|(service, group_pods)| { + let ready_count = group_pods.iter().filter(|p| p.ready).count(); + let total_count = group_pods.len(); PodGroupData { service, - pods, + pods: group_pods, ready_count, total_count, } diff --git a/bin/fxt/src/tui/event_loop.rs b/bin/fxt/src/tui/event_loop.rs index 365ff3911..6b5eba858 100644 --- a/bin/fxt/src/tui/event_loop.rs +++ b/bin/fxt/src/tui/event_loop.rs @@ -253,10 +253,12 @@ fn apply_update(state: &mut AppState, update: StateUpdate) { sessions, gpu, resources, + active_jobs, } => { state.training_sessions = sessions; state.gpu = gpu; state.system_resources = resources; + state.active_jobs = active_jobs; } StateUpdate::Services(services) => { state.services = services; diff --git a/bin/fxt/src/tui/state.rs b/bin/fxt/src/tui/state.rs index c0f602a89..3ace21dce 100644 --- a/bin/fxt/src/tui/state.rs +++ b/bin/fxt/src/tui/state.rs @@ -37,6 +37,8 @@ pub struct AppState { pub show_help: bool, /// ML training sessions. pub training_sessions: Vec, + /// Active K8s training jobs. + pub active_jobs: u32, /// GPU telemetry. pub gpu: GpuData, /// Service health statuses. @@ -81,6 +83,7 @@ impl Default for AppState { current_cockpit: 0, show_help: false, training_sessions: Vec::new(), + active_jobs: 0, gpu: GpuData::default(), services: Vec::new(), positions: Vec::new(), @@ -155,6 +158,8 @@ pub struct ServiceData { pub version: String, /// Health-check round-trip latency in milliseconds. pub latency_ms: f64, + /// Error reason when service is unhealthy. + pub error: Option, } /// An open trading position. diff --git a/bin/fxt/tests/error_tests.rs b/bin/fxt/tests/error_tests.rs index 490dba3d2..5acdbc4d3 100644 --- a/bin/fxt/tests/error_tests.rs +++ b/bin/fxt/tests/error_tests.rs @@ -1,6 +1,6 @@ //! Unit tests for error module //! -//! Tests all TliError variants and error handling functionality. +//! Tests all FxtError variants and error handling functionality. // Suppress false-positive unused_crate_dependencies warnings // dev-dependencies are shared across ALL test targets in the crate @@ -8,127 +8,127 @@ #![allow(unused_crate_dependencies)] use std::io; -use fxt::error::{TliError, TliResult}; +use fxt::error::{FxtError, FxtResult}; -/// Test TliError::Connection variant +/// Test FxtError::Connection variant #[test] -fn test_tli_error_connection() { - let error = TliError::Connection("connection failed".to_string()); +fn test_fxt_error_connection() { + let error = FxtError::Connection("connection failed".to_string()); - assert!(matches!(error, TliError::Connection(_))); + assert!(matches!(error, FxtError::Connection(_))); assert_eq!(error.to_string(), "Connection error: connection failed"); } -/// Test TliError::Config variant +/// Test FxtError::Config variant #[test] -fn test_tli_error_config() { - let error = TliError::Config("invalid config".to_string()); +fn test_fxt_error_config() { + let error = FxtError::Config("invalid config".to_string()); - assert!(matches!(error, TliError::Config(_))); + assert!(matches!(error, FxtError::Config(_))); assert_eq!(error.to_string(), "Configuration error: invalid config"); } -/// Test TliError::Dashboard variant +/// Test FxtError::Dashboard variant #[test] -fn test_tli_error_dashboard() { - let error = TliError::Dashboard("render failed".to_string()); +fn test_fxt_error_dashboard() { + let error = FxtError::Dashboard("render failed".to_string()); - assert!(matches!(error, TliError::Dashboard(_))); + assert!(matches!(error, FxtError::Dashboard(_))); assert_eq!(error.to_string(), "Dashboard error: render failed"); } -/// Test TliError::Io variant from std::io::Error +/// Test FxtError::Io variant from std::io::Error #[test] -fn test_tli_error_io() { +fn test_fxt_error_io() { let io_error = io::Error::new(io::ErrorKind::NotFound, "file not found"); - let error = TliError::from(io_error); + let error = FxtError::from(io_error); - assert!(matches!(error, TliError::Io(_))); + assert!(matches!(error, FxtError::Io(_))); assert!(error.to_string().contains("file not found")); } -/// Test TliError::Grpc variant from tonic::Status +/// Test FxtError::Grpc variant from tonic::Status #[test] -fn test_tli_error_grpc() { +fn test_fxt_error_grpc() { let status = tonic::Status::unavailable("service unavailable"); - let error = TliError::from(status); + let error = FxtError::from(status); - assert!(matches!(error, TliError::Grpc(_))); + assert!(matches!(error, FxtError::Grpc(_))); assert!(error.to_string().contains("service unavailable")); } -/// Test TliError::InvalidRequest variant +/// Test FxtError::InvalidRequest variant #[test] -fn test_tli_error_invalid_request() { - let error = TliError::InvalidRequest("negative quantity".to_string()); +fn test_fxt_error_invalid_request() { + let error = FxtError::InvalidRequest("negative quantity".to_string()); - assert!(matches!(error, TliError::InvalidRequest(_))); + assert!(matches!(error, FxtError::InvalidRequest(_))); assert_eq!(error.to_string(), "Invalid request: negative quantity"); } -/// Test TliError::NotFound variant +/// Test FxtError::NotFound variant #[test] -fn test_tli_error_not_found() { - let error = TliError::NotFound("order not found".to_string()); +fn test_fxt_error_not_found() { + let error = FxtError::NotFound("order not found".to_string()); - assert!(matches!(error, TliError::NotFound(_))); + assert!(matches!(error, FxtError::NotFound(_))); assert_eq!(error.to_string(), "Not found: order not found"); } -/// Test TliError::BufferFull variant +/// Test FxtError::BufferFull variant #[test] -fn test_tli_error_buffer_full() { - let error = TliError::BufferFull("event buffer full".to_string()); +fn test_fxt_error_buffer_full() { + let error = FxtError::BufferFull("event buffer full".to_string()); - assert!(matches!(error, TliError::BufferFull(_))); + assert!(matches!(error, FxtError::BufferFull(_))); assert_eq!(error.to_string(), "Buffer full: event buffer full"); } -/// Test TliError::InvalidSymbol variant +/// Test FxtError::InvalidSymbol variant #[test] -fn test_tli_error_invalid_symbol() { - let error = TliError::InvalidSymbol("INVALID@123".to_string()); +fn test_fxt_error_invalid_symbol() { + let error = FxtError::InvalidSymbol("INVALID@123".to_string()); - assert!(matches!(error, TliError::InvalidSymbol(_))); + assert!(matches!(error, FxtError::InvalidSymbol(_))); assert_eq!(error.to_string(), "Invalid symbol: INVALID@123"); } -/// Test TliError::Other variant +/// Test FxtError::Other variant #[test] -fn test_tli_error_other() { - let error = TliError::Other("unexpected error".to_string()); +fn test_fxt_error_other() { + let error = FxtError::Other("unexpected error".to_string()); - assert!(matches!(error, TliError::Other(_))); + assert!(matches!(error, FxtError::Other(_))); assert_eq!(error.to_string(), "Other error: unexpected error"); } -/// Test TliError debug formatting +/// Test FxtError debug formatting #[test] -fn test_tli_error_debug() { - let error = TliError::Connection("test".to_string()); +fn test_fxt_error_debug() { + let error = FxtError::Connection("test".to_string()); let debug_str = format!("{:?}", error); assert!(debug_str.contains("Connection")); assert!(debug_str.contains("test")); } -/// Test TliResult with Ok value +/// Test FxtResult with Ok value #[test] -fn test_tli_result_ok() { - let result: TliResult = Ok(42); +fn test_fxt_result_ok() { + let result: FxtResult = Ok(42); assert!(result.is_ok()); assert_eq!(result.unwrap(), 42); } -/// Test TliResult with Err value +/// Test FxtResult with Err value #[test] -fn test_tli_result_err() { - let result: TliResult = Err(TliError::Connection("failed".to_string())); +fn test_fxt_result_err() { + let result: FxtResult = Err(FxtError::Connection("failed".to_string())); assert!(result.is_err()); let error = result.unwrap_err(); - assert!(matches!(error, TliError::Connection(_))); + assert!(matches!(error, FxtError::Connection(_))); } /// Test error message formatting @@ -136,19 +136,19 @@ fn test_tli_result_err() { fn test_error_message_formatting() { let errors = vec![ ( - TliError::Connection("timeout".to_string()), + FxtError::Connection("timeout".to_string()), "Connection error: timeout", ), ( - TliError::Config("missing field".to_string()), + FxtError::Config("missing field".to_string()), "Configuration error: missing field", ), ( - TliError::InvalidRequest("invalid data".to_string()), + FxtError::InvalidRequest("invalid data".to_string()), "Invalid request: invalid data", ), ( - TliError::NotFound("resource".to_string()), + FxtError::NotFound("resource".to_string()), "Not found: resource", ), ]; @@ -161,7 +161,7 @@ fn test_error_message_formatting() { /// Test error with empty message #[test] fn test_error_with_empty_message() { - let error = TliError::Connection("".to_string()); + let error = FxtError::Connection("".to_string()); assert_eq!(error.to_string(), "Connection error: "); } @@ -169,7 +169,7 @@ fn test_error_with_empty_message() { #[test] fn test_error_with_long_message() { let long_message = "a".repeat(10000); - let error = TliError::Other(long_message.clone()); + let error = FxtError::Other(long_message.clone()); assert!(error.to_string().contains(&long_message)); } @@ -178,7 +178,7 @@ fn test_error_with_long_message() { #[test] fn test_error_with_special_characters() { let message = "Error: timeout (500ms) at server://host:8080"; - let error = TliError::Connection(message.to_string()); + let error = FxtError::Connection(message.to_string()); assert!(error.to_string().contains(message)); } @@ -195,9 +195,9 @@ fn test_error_from_different_io_kinds() { for kind in kinds { let io_error = io::Error::new(kind, "test error"); - let tli_error = TliError::from(io_error); + let fxt_error = FxtError::from(io_error); - assert!(matches!(tli_error, TliError::Io(_))); + assert!(matches!(fxt_error, FxtError::Io(_))); } } @@ -213,19 +213,19 @@ fn test_error_from_different_grpc_codes() { ]; for status in statuses { - let tli_error = TliError::from(status); - assert!(matches!(tli_error, TliError::Grpc(_))); + let fxt_error = FxtError::from(status); + assert!(matches!(fxt_error, FxtError::Grpc(_))); } } /// Test error propagation in functions #[test] fn test_error_propagation() { - fn operation_that_fails() -> TliResult<()> { - Err(TliError::Connection("failed".to_string())) + fn operation_that_fails() -> FxtResult<()> { + Err(FxtError::Connection("failed".to_string())) } - fn caller() -> TliResult<()> { + fn caller() -> FxtResult<()> { operation_that_fails()?; Ok(()) } @@ -238,18 +238,18 @@ fn test_error_propagation() { #[test] fn test_error_pattern_matching() { let errors = vec![ - TliError::Connection("test".to_string()), - TliError::Config("test".to_string()), - TliError::InvalidRequest("test".to_string()), - TliError::NotFound("test".to_string()), + FxtError::Connection("test".to_string()), + FxtError::Config("test".to_string()), + FxtError::InvalidRequest("test".to_string()), + FxtError::NotFound("test".to_string()), ]; for error in errors { match error { - TliError::Connection(_) => assert!(true), - TliError::Config(_) => assert!(true), - TliError::InvalidRequest(_) => assert!(true), - TliError::NotFound(_) => assert!(true), + FxtError::Connection(_) => assert!(true), + FxtError::Config(_) => assert!(true), + FxtError::InvalidRequest(_) => assert!(true), + FxtError::NotFound(_) => assert!(true), _ => assert!(false, "unexpected variant"), } } @@ -259,7 +259,7 @@ fn test_error_pattern_matching() { #[test] fn test_error_with_newlines() { let message = "Line 1\nLine 2\nLine 3"; - let error = TliError::Other(message.to_string()); + let error = FxtError::Other(message.to_string()); assert!(error.to_string().contains("Line 1")); assert!(error.to_string().contains("Line 2")); @@ -269,7 +269,7 @@ fn test_error_with_newlines() { #[test] fn test_error_with_unicode() { let message = "Error: 失败 (Chinese), エラー (Japanese), ошибка (Russian)"; - let error = TliError::Connection(message.to_string()); + let error = FxtError::Connection(message.to_string()); assert!(error.to_string().contains("失败")); assert!(error.to_string().contains("エラー")); @@ -278,11 +278,11 @@ fn test_error_with_unicode() { /// Test Result type aliases #[test] fn test_result_type_alias() { - fn returns_tli_result() -> TliResult { + fn returns_fxt_result() -> FxtResult { Ok("success".to_string()) } - let result = returns_tli_result(); + let result = returns_fxt_result(); assert!(result.is_ok()); assert_eq!(result.unwrap(), "success"); } @@ -294,14 +294,14 @@ fn test_error_chain() { Err(io::Error::new(io::ErrorKind::NotFound, "inner error")) } - fn outer_operation() -> TliResult<()> { - inner_operation().map_err(|e| TliError::from(e))?; + fn outer_operation() -> FxtResult<()> { + inner_operation().map_err(|e| FxtError::from(e))?; Ok(()) } let result = outer_operation(); assert!(result.is_err()); - assert!(matches!(result.unwrap_err(), TliError::Io(_))); + assert!(matches!(result.unwrap_err(), FxtError::Io(_))); } /// Test InvalidSymbol with various invalid symbols @@ -317,8 +317,8 @@ fn test_invalid_symbol_variants() { ]; for symbol in invalid_symbols { - let error = TliError::InvalidSymbol(symbol.to_string()); - assert!(matches!(error, TliError::InvalidSymbol(_))); + let error = FxtError::InvalidSymbol(symbol.to_string()); + assert!(matches!(error, FxtError::InvalidSymbol(_))); } } @@ -333,15 +333,15 @@ fn test_buffer_full_variants() { ]; for buffer_type in buffer_types { - let error = TliError::BufferFull(format!("{} full", buffer_type)); - assert!(matches!(error, TliError::BufferFull(_))); + let error = FxtError::BufferFull(format!("{} full", buffer_type)); + assert!(matches!(error, FxtError::BufferFull(_))); } } /// Test error unwrap_or_else with fallback #[test] fn test_error_unwrap_or_else() { - let result: TliResult = Err(TliError::NotFound("test".to_string())); + let result: FxtResult = Err(FxtError::NotFound("test".to_string())); let value = result.unwrap_or_else(|_| 42); assert_eq!(value, 42); @@ -350,7 +350,7 @@ fn test_error_unwrap_or_else() { /// Test error map_err transformation #[test] fn test_error_map_err() { - let result: TliResult = Err(TliError::Connection("test".to_string())); + let result: FxtResult = Err(FxtError::Connection("test".to_string())); let transformed = result.map_err(|e| format!("Transformed: {}", e)); assert!(transformed.is_err()); diff --git a/bin/fxt/tests/integration/end_to_end_tests.rs b/bin/fxt/tests/integration/end_to_end_tests.rs deleted file mode 100644 index ebb742ff2..000000000 --- a/bin/fxt/tests/integration/end_to_end_tests.rs +++ /dev/null @@ -1,1570 +0,0 @@ -//! End-to-end integration tests for TLI system -//! -//! This module tests complete workflows including order lifecycle management, -//! backtesting workflows, configuration management, and security authentication flows. - -use chrono::{DateTime, Utc}; -use fake::{Fake, Faker}; -use std::collections::HashMap; -use std::time::Duration; -use tokio::time::{sleep, timeout}; -use uuid::Uuid; - -use crate::integration::{TestConfig, TestUtilities}; -use crate::mocks::grpc_server::{MockBacktestingServer, MockRiskServer, MockTradingServer}; -// Auth types removed - TLI is pure client -// use fxt::auth::{AuthenticationManager, RbacManager, SessionManager}; -use fxt::client::{TliClientBuilder, TliClientSuite}; -// Database imports removed - TLI is pure client -use fxt::prelude::*; - -/// End-to-end test environment -pub struct EndToEndTestEnvironment { - config: TestConfig, - client_suite: Option, - mock_servers: Vec>, - // Auth managers removed - TLI is pure client - // auth_manager: Option, - // session_manager: Option, - config_manager: Option, -} - -impl EndToEndTestEnvironment { - pub fn new(config: TestConfig) -> Self { - Self { - config, - client_suite: None, - mock_servers: Vec::new(), - // Auth managers removed - TLI is pure client - // auth_manager: None, - // session_manager: None, - config_manager: None, - } - } - - /// Setup complete end-to-end test environment - pub async fn setup(&mut self) -> TliResult<()> { - tracing::info!("Setting up end-to-end test environment"); - - // Start mock services - self.start_mock_services().await?; - - // Setup authentication and session management - DISABLED (TLI is pure client) - // self.setup_auth_services().await?; - - // Setup configuration management - self.setup_config_management().await?; - - // Create TLI client suite - self.setup_client_suite().await?; - - tracing::info!("End-to-end test environment setup complete"); - Ok(()) - } - - async fn start_mock_services(&mut self) -> TliResult<()> { - let base_port = self.config.mock_server_port; - - // Start trading service - let mut trading_server = MockTradingServer::new(base_port)?; - trading_server.start()?; - self.mock_servers.push(Box::new(trading_server)); - - // Start backtesting service - let mut backtesting_server = MockBacktestingServer::new(base_port + 10)?; - backtesting_server.start()?; - self.mock_servers.push(Box::new(backtesting_server)); - - // Start risk management service - let mut risk_server = MockRiskServer::new(base_port + 20)?; - risk_server.start()?; - self.mock_servers.push(Box::new(risk_server)); - - // Wait for services to be ready - sleep(Duration::from_millis(500)).await; - - Ok(()) - } - - // Auth setup disabled - TLI is pure client - // async fn setup_auth_services(&mut self) -> TliResult<()> { ... } - - async fn setup_config_management(&mut self) -> TliResult<()> { - // Create temporary database for testing - let temp_dir = tempfile::TempDir::new().map_err(|e| TliError::Database(e.to_string()))?; - let db_path = temp_dir.path().join("test_config.db"); - let db_url = format!("sqlite:{}", db_path.display()); - - let config_db_config = DatabaseConfig { - url: db_url, - max_connections: 5, - connection_timeout: Duration::from_secs(10), - idle_timeout: Some(Duration::from_secs(300)), - max_lifetime: Some(Duration::from_secs(1800)), - }; - - self.config_manager = Some(ConfigurationManager::new(config_db_config).await?); - - Ok(()) - } - - async fn setup_client_suite(&mut self) -> TliResult<()> { - let base_port = self.config.mock_server_port; - - let client_suite = TliClientBuilder::new() - .with_service_endpoint( - "trading_service".to_string(), - format!("http://localhost:{}", base_port), - ) - .with_service_endpoint( - "backtesting_service".to_string(), - format!("http://localhost:{}", base_port + 10), - ) - .with_service_endpoint( - "risk_service".to_string(), - format!("http://localhost:{}", base_port + 20), - ) - .with_trading_config(TradingClientConfig::default()) - .with_backtesting_config(BacktestingClientConfig::default()) - .build() - .await?; - - self.client_suite = Some(client_suite); - - Ok(()) - } - - /// Cleanup test environment - pub async fn teardown(&mut self) -> TliResult<()> { - tracing::info!("Tearing down end-to-end test environment"); - - // Shutdown client suite - if let Some(client_suite) = self.client_suite.take() { - client_suite.shutdown().await; - } - - // Stop mock servers - for server in &mut self.mock_servers { - let _ = server.stop(); - } - self.mock_servers.clear(); - - tracing::info!("End-to-end test environment teardown complete"); - Ok(()) - } - - pub fn get_trading_client(&self) -> &TradingClient { - self.client_suite - .as_ref() - .expect("Client suite not initialized") - .trading_client - .as_ref() - .expect("Trading client not available") - } - - pub fn get_backtesting_client(&self) -> &BacktestingClient { - self.client_suite - .as_ref() - .expect("Client suite not initialized") - .backtesting_client - .as_ref() - .expect("Backtesting client not available") - } -} - -/// Complete order lifecycle tests -#[cfg(test)] -mod order_lifecycle_tests { - use super::*; - - #[tokio::test] - async fn test_complete_order_lifecycle() { - let mut test_env = EndToEndTestEnvironment::new(TestConfig::default()); - test_env - .setup() - .await - .expect("Failed to setup test environment"); - - let trading_client = test_env.get_trading_client(); - let symbol = TestUtilities::generate_test_symbol("AAPL"); - let client_order_id = Uuid::new_v4().to_string(); - - // Step 1: Pre-trade risk check - let risk_check_request = PreTradeRiskCheckRequest { - symbol: symbol.clone(), - side: OrderSide::Buy as i32, - quantity: 100.0, - estimated_price: Some(150.0), - order_type: OrderType::Limit as i32, - account_id: "test_account".to_string(), - }; - - let risk_response = trading_client - .check_pre_trade_risk(risk_check_request) - .await - .expect("Failed to perform pre-trade risk check"); - - assert!(risk_response.approved, "Order should pass risk checks"); - tracing::info!( - "Pre-trade risk check passed with score: {}", - risk_response.risk_score - ); - - // Step 2: Submit order - let order_request = SubmitOrderRequest { - symbol: symbol.clone(), - side: OrderSide::Buy as i32, - order_type: OrderType::Limit as i32, - quantity: 100.0, - price: Some(150.0), - stop_price: None, - time_in_force: "DAY".to_string(), - client_order_id: client_order_id.clone(), - }; - - let submit_response = trading_client - .submit_order(order_request) - .await - .expect("Failed to submit order"); - - assert!(submit_response.success, "Order submission should succeed"); - assert!( - !submit_response.order_id.is_empty(), - "Order ID should be provided" - ); - - let order_id = submit_response.order_id.clone(); - tracing::info!("Order submitted successfully: {}", order_id); - - // Step 3: Monitor order status - let mut order_status = OrderStatus::Pending; - let mut status_checks = 0; - let max_status_checks = 10; - - while status_checks < max_status_checks && order_status != OrderStatus::Filled { - let status_request = GetOrderStatusRequest { - order_id: order_id.clone(), - include_fills: true, - }; - - let status_response = trading_client - .get_order_status(status_request) - .await - .expect("Failed to get order status"); - - order_status = - OrderStatus::from_i32(status_response.status).unwrap_or(OrderStatus::Unknown); - - tracing::info!( - "Order status check {}: {:?}", - status_checks + 1, - order_status - ); - - if order_status == OrderStatus::Filled { - assert!( - !status_response.fills.is_empty(), - "Filled order should have fills" - ); - break; - } - - status_checks += 1; - sleep(Duration::from_millis(100)).await; - } - - // Step 4: Check position update - let position_request = GetPositionsRequest { - account_id: Some("test_account".to_string()), - symbol_filter: Some(symbol.clone()), - include_zero_positions: false, - }; - - let positions_response = trading_client - .get_positions(position_request) - .await - .expect("Failed to get positions"); - - let position = positions_response - .positions - .iter() - .find(|p| p.symbol == symbol) - .expect("Should have position for traded symbol"); - - assert!( - position.quantity > 0.0, - "Position quantity should be positive after buy" - ); - tracing::info!( - "Position updated: {} shares of {}", - position.quantity, - symbol - ); - - // Step 5: Check portfolio analytics - let analytics_request = PortfolioAnalyticsRequest { - account_id: "test_account".to_string(), - calculation_date: Utc::now().timestamp(), - include_realized_pnl: true, - include_unrealized_pnl: true, - include_risk_metrics: true, - }; - - let analytics_response = trading_client - .get_portfolio_analytics(analytics_request) - .await - .expect("Failed to get portfolio analytics"); - - assert!( - analytics_response.total_portfolio_value.is_some(), - "Portfolio value should be calculated" - ); - tracing::info!("Portfolio analytics updated successfully"); - - // Step 6: Place offsetting order (sell) - let sell_request = SubmitOrderRequest { - symbol: symbol.clone(), - side: OrderSide::Sell as i32, - order_type: OrderType::Market as i32, - quantity: 100.0, - price: None, - stop_price: None, - time_in_force: "DAY".to_string(), - client_order_id: Uuid::new_v4().to_string(), - }; - - let sell_response = trading_client - .submit_order(sell_request) - .await - .expect("Failed to submit sell order"); - - assert!(sell_response.success, "Sell order should succeed"); - tracing::info!( - "Offsetting sell order submitted: {}", - sell_response.order_id - ); - - test_env - .teardown() - .await - .expect("Failed to teardown test environment"); - } - - #[tokio::test] - async fn test_order_modification_lifecycle() { - let mut test_env = EndToEndTestEnvironment::new(TestConfig::default()); - test_env - .setup() - .await - .expect("Failed to setup test environment"); - - let trading_client = test_env.get_trading_client(); - let symbol = TestUtilities::generate_test_symbol("GOOGL"); - - // Submit initial order - let order_request = SubmitOrderRequest { - symbol: symbol.clone(), - side: OrderSide::Buy as i32, - order_type: OrderType::Limit as i32, - quantity: 50.0, - price: Some(2800.0), - stop_price: None, - time_in_force: "GTC".to_string(), - client_order_id: Uuid::new_v4().to_string(), - }; - - let submit_response = trading_client - .submit_order(order_request) - .await - .expect("Failed to submit order"); - - let order_id = submit_response.order_id.clone(); - - // Modify order price - let modify_request = ModifyOrderRequest { - order_id: order_id.clone(), - new_quantity: Some(75.0), - new_price: Some(2850.0), - new_stop_price: None, - }; - - let modify_response = trading_client - .modify_order(modify_request) - .await - .expect("Failed to modify order"); - - assert!(modify_response.success, "Order modification should succeed"); - - // Verify modification - let status_request = GetOrderStatusRequest { - order_id: order_id.clone(), - include_fills: false, - }; - - let status_response = trading_client - .get_order_status(status_request) - .await - .expect("Failed to get order status"); - - assert_eq!(status_response.quantity, 75.0, "Quantity should be updated"); - assert_eq!( - status_response.price.unwrap(), - 2850.0, - "Price should be updated" - ); - - // Cancel order - let cancel_request = CancelOrderRequest { - order_id: order_id.clone(), - symbol: symbol.clone(), - }; - - let cancel_response = trading_client - .cancel_order(cancel_request) - .await - .expect("Failed to cancel order"); - - assert!(cancel_response.success, "Order cancellation should succeed"); - - test_env - .teardown() - .await - .expect("Failed to teardown test environment"); - } - - #[tokio::test] - async fn test_batch_order_operations() { - let mut test_env = EndToEndTestEnvironment::new(TestConfig::default()); - test_env - .setup() - .await - .expect("Failed to setup test environment"); - - let trading_client = test_env.get_trading_client(); - - // Create batch of orders - let mut batch_orders = Vec::new(); - let symbols = ["AAPL", "GOOGL", "MSFT", "TSLA", "AMZN"]; - - for (i, symbol) in symbols.into_iter().enumerate() { - batch_orders.push(SubmitOrderRequest { - symbol: TestUtilities::generate_test_symbol(symbol), - side: OrderSide::Buy as i32, - order_type: OrderType::Limit as i32, - quantity: (i + 1) as f64 * 100.0, - price: Some(100.0 + (i as f64 * 50.0)), - stop_price: None, - time_in_force: "DAY".to_string(), - client_order_id: Uuid::new_v4().to_string(), - }); - } - - // Submit batch orders - let batch_request = SubmitBatchOrdersRequest { - orders: batch_orders, - all_or_none: false, - max_acceptable_failures: 1, - }; - - let batch_response = trading_client - .submit_batch_orders(batch_request) - .await - .expect("Failed to submit batch orders"); - - assert!( - batch_response.success, - "Batch order submission should succeed" - ); - assert_eq!( - batch_response.order_results.len(), - 5, - "Should have results for all orders" - ); - - // Verify individual order results - let successful_orders: Vec<_> = batch_response - .order_results - .iter() - .filter(|result| result.success) - .collect(); - - assert!(successful_orders.len() >= 4, "Most orders should succeed"); - - // Get batch order status - let order_ids: Vec = successful_orders - .iter() - .map(|result| result.order_id.clone()) - .collect(); - - let batch_status_request = GetBatchOrderStatusRequest { - order_ids: order_ids.clone(), - }; - - let batch_status_response = trading_client - .get_batch_order_status(batch_status_request) - .await - .expect("Failed to get batch order status"); - - assert_eq!( - batch_status_response.order_statuses.len(), - order_ids.len(), - "Should have status for all orders" - ); - - test_env - .teardown() - .await - .expect("Failed to teardown test environment"); - } -} - -/// Backtesting workflow tests -#[cfg(test)] -mod backtesting_workflow_tests { - use super::*; - - #[tokio::test] - async fn test_complete_backtesting_workflow() { - let mut test_env = EndToEndTestEnvironment::new(TestConfig::default()); - test_env - .setup() - .await - .expect("Failed to setup test environment"); - - let backtesting_client = test_env.get_backtesting_client(); - - // Step 1: Check data availability - let data_check_request = CheckDataAvailabilityRequest { - symbols: vec!["AAPL".to_string(), "GOOGL".to_string()], - start_date: "2024-01-01".to_string(), - end_date: "2024-01-31".to_string(), - data_types: vec![DataType::OHLCV as i32, DataType::Trades as i32], - }; - - let data_availability = backtesting_client - .check_data_availability(data_check_request) - .await - .expect("Failed to check data availability"); - - assert!( - !data_availability.symbol_availability.is_empty(), - "Should have data availability info" - ); - tracing::info!( - "Data availability confirmed for {} symbols", - data_availability.symbol_availability.len() - ); - - // Step 2: Create backtest configuration - let mut strategy_parameters = HashMap::new(); - strategy_parameters.insert("lookback_period".to_string(), "20".to_string()); - strategy_parameters.insert("threshold".to_string(), "0.02".to_string()); - - let backtest_request = CreateBacktestRequest { - name: format!("E2E Test Backtest {}", Uuid::new_v4()), - strategy_id: "mean_reversion_strategy".to_string(), - start_date: "2024-01-01".to_string(), - end_date: "2024-01-31".to_string(), - initial_capital: 100000.0, - symbols: vec!["AAPL".to_string(), "GOOGL".to_string()], - parameters: strategy_parameters, - }; - - let create_response = backtesting_client - .create_backtest(backtest_request) - .await - .expect("Failed to create backtest"); - - assert!(create_response.success, "Backtest creation should succeed"); - let backtest_id = create_response.backtest_id.clone(); - tracing::info!("Backtest created: {}", backtest_id); - - // Step 3: Start backtest execution - let start_request = StartBacktestRequest { - backtest_id: backtest_id.clone(), - async_execution: true, - }; - - let start_response = backtesting_client - .start_backtest(start_request) - .await - .expect("Failed to start backtest"); - - assert!(start_response.success, "Backtest start should succeed"); - tracing::info!("Backtest execution started"); - - // Step 4: Monitor backtest progress - let mut progress_percentage = 0.0; - let mut status_checks = 0; - let max_status_checks = 20; - - while status_checks < max_status_checks && progress_percentage < 100.0 { - let status_request = GetBacktestStatusRequest { - backtest_id: backtest_id.clone(), - }; - - let status_response = backtesting_client - .get_backtest_status(status_request) - .await - .expect("Failed to get backtest status"); - - progress_percentage = status_response.progress_percentage; - let status = - BacktestStatus::from_i32(status_response.status).unwrap_or(BacktestStatus::Unknown); - - tracing::info!( - "Backtest progress: {}%, Status: {:?}", - progress_percentage, - status - ); - - if status == BacktestStatus::Completed { - break; - } - - if status == BacktestStatus::Failed { - panic!( - "Backtest failed: {}", - status_response.error_message.unwrap_or_default() - ); - } - - status_checks += 1; - sleep(Duration::from_millis(200)).await; - } - - // Step 5: Retrieve backtest results - let results_request = GetBacktestResultsRequest { - backtest_id: backtest_id.clone(), - include_trades: true, - include_metrics: true, - }; - - let results_response = backtesting_client - .get_backtest_results(results_request) - .await - .expect("Failed to get backtest results"); - - // Validate results - assert!( - results_response.performance_summary.is_some(), - "Should have performance summary" - ); - let performance = results_response.performance_summary.unwrap(); - - assert!( - performance.total_return != 0.0, - "Should have calculated total return" - ); - assert!( - performance.sharpe_ratio.is_some(), - "Should have Sharpe ratio" - ); - assert!( - performance.max_drawdown.is_some(), - "Should have max drawdown" - ); - - assert!( - !results_response.trades.is_empty(), - "Should have executed trades" - ); - tracing::info!( - "Backtest completed with {} trades", - results_response.trades.len() - ); - - // Step 6: Generate backtest report - let report_request = GenerateBacktestReportRequest { - backtest_id: backtest_id.clone(), - report_format: ReportFormat::Pdf as i32, - include_charts: true, - include_trade_details: true, - }; - - let report_response = backtesting_client - .generate_backtest_report(report_request) - .await - .expect("Failed to generate backtest report"); - - assert!(report_response.success, "Report generation should succeed"); - assert!( - !report_response.report_url.is_empty(), - "Should have report URL" - ); - tracing::info!("Backtest report generated: {}", report_response.report_url); - - test_env - .teardown() - .await - .expect("Failed to teardown test environment"); - } - - #[tokio::test] - async fn test_strategy_optimization_workflow() { - let mut test_env = EndToEndTestEnvironment::new(TestConfig::default()); - test_env - .setup() - .await - .expect("Failed to setup test environment"); - - let backtesting_client = test_env.get_backtesting_client(); - - // Define parameter ranges for optimization - let mut parameter_ranges = HashMap::new(); - parameter_ranges.insert( - "lookback_period".to_string(), - ParameterRange { - min_value: 10.0, - max_value: 50.0, - step_size: 5.0, - parameter_type: ParameterType::Integer as i32, - }, - ); - - parameter_ranges.insert( - "threshold".to_string(), - ParameterRange { - min_value: 0.01, - max_value: 0.05, - step_size: 0.005, - parameter_type: ParameterType::Float as i32, - }, - ); - - // Start optimization - let optimization_request = StrategyOptimizationRequest { - strategy_id: "mean_reversion_strategy".to_string(), - start_date: "2024-01-01".to_string(), - end_date: "2024-02-29".to_string(), - symbols: vec!["AAPL".to_string()], - parameter_ranges, - optimization_metric: OptimizationMetric::SharpeRatio as i32, - max_iterations: 20, - }; - - let optimization_response = backtesting_client - .optimize_strategy(optimization_request) - .await - .expect("Failed to start strategy optimization"); - - assert!( - optimization_response.success, - "Optimization should start successfully" - ); - let optimization_id = optimization_response.optimization_id.clone(); - tracing::info!("Strategy optimization started: {}", optimization_id); - - // Monitor optimization progress - let mut iterations_completed = 0; - let mut status_checks = 0; - let max_status_checks = 30; - - while status_checks < max_status_checks && iterations_completed < 20 { - let status_request = GetOptimizationStatusRequest { - optimization_id: optimization_id.clone(), - }; - - let status_response = backtesting_client - .get_optimization_status(status_request) - .await - .expect("Failed to get optimization status"); - - iterations_completed = status_response.iterations_completed; - let status = OptimizationStatus::from_i32(status_response.status) - .unwrap_or(OptimizationStatus::Unknown); - - tracing::info!( - "Optimization progress: {}/{} iterations, Status: {:?}", - iterations_completed, - 20, - status - ); - - if status == OptimizationStatus::Completed { - break; - } - - status_checks += 1; - sleep(Duration::from_millis(300)).await; - } - - // Get optimization results - let results_request = GetOptimizationResultsRequest { - optimization_id: optimization_id.clone(), - }; - - let results_response = backtesting_client - .get_optimization_results(results_request) - .await - .expect("Failed to get optimization results"); - - assert!( - !results_response.parameter_combinations.is_empty(), - "Should have parameter combinations" - ); - assert!( - results_response.best_parameters.is_some(), - "Should have best parameters" - ); - - let best_params = results_response.best_parameters.unwrap(); - tracing::info!( - "Best optimization result: Sharpe Ratio = {}, Parameters: {:?}", - best_params.metric_value, - best_params.parameters - ); - - test_env - .teardown() - .await - .expect("Failed to teardown test environment"); - } - - #[tokio::test] - async fn test_walk_forward_analysis_workflow() { - let mut test_env = EndToEndTestEnvironment::new(TestConfig::default()); - test_env - .setup() - .await - .expect("Failed to setup test environment"); - - let backtesting_client = test_env.get_backtesting_client(); - - // Setup walk-forward analysis - let walk_forward_request = CreateWalkForwardAnalysisRequest { - strategy_id: "mean_reversion_strategy".to_string(), - start_date: "2024-01-01".to_string(), - end_date: "2024-06-30".to_string(), - symbols: vec!["AAPL".to_string()], - in_sample_period_days: 60, - out_of_sample_period_days: 30, - optimization_metric: OptimizationMetric::SharpeRatio as i32, - reoptimization_frequency_days: 30, - }; - - let walk_forward_response = backtesting_client - .create_walk_forward_analysis(walk_forward_request) - .await - .expect("Failed to start walk-forward analysis"); - - assert!( - walk_forward_response.success, - "Walk-forward analysis should start" - ); - let analysis_id = walk_forward_response.analysis_id.clone(); - - // Monitor analysis progress - let mut periods_completed = 0; - let mut status_checks = 0; - let max_status_checks = 40; - - while status_checks < max_status_checks { - let status_request = GetWalkForwardStatusRequest { - analysis_id: analysis_id.clone(), - }; - - let status_response = backtesting_client - .get_walk_forward_status(status_request) - .await - .expect("Failed to get walk-forward status"); - - periods_completed = status_response.periods_completed; - let status = WalkForwardStatus::from_i32(status_response.status) - .unwrap_or(WalkForwardStatus::Unknown); - - tracing::info!( - "Walk-forward progress: {} periods completed, Status: {:?}", - periods_completed, - status - ); - - if status == WalkForwardStatus::Completed { - break; - } - - status_checks += 1; - sleep(Duration::from_millis(500)).await; - } - - // Get walk-forward results - let results_request = GetWalkForwardResultsRequest { - analysis_id: analysis_id.clone(), - }; - - let results_response = backtesting_client - .get_walk_forward_results(results_request) - .await - .expect("Failed to get walk-forward results"); - - assert!( - !results_response.period_results.is_empty(), - "Should have period results" - ); - assert!( - results_response.aggregate_statistics.is_some(), - "Should have aggregate statistics" - ); - - let aggregate_stats = results_response.aggregate_statistics.unwrap(); - tracing::info!( - "Walk-forward analysis completed: Overall Sharpe = {}, Stability = {}", - aggregate_stats.average_sharpe_ratio, - aggregate_stats.stability_metric - ); - - test_env - .teardown() - .await - .expect("Failed to teardown test environment"); - } -} - -/// Configuration management flow tests -#[cfg(test)] -mod configuration_flow_tests { - use super::*; - - #[tokio::test] - async fn test_configuration_update_with_hot_reload() { - let mut test_env = EndToEndTestEnvironment::new(TestConfig::default()); - test_env - .setup() - .await - .expect("Failed to setup test environment"); - - let trading_client = test_env.get_trading_client(); - - // Step 1: Get current configuration - let get_config_request = GetConfigurationRequest { - config_type: ConfigurationType::TradingLimits as i32, - account_id: Some("test_account".to_string()), - }; - - let initial_config = trading_client - .get_configuration(get_config_request.clone()) - .await - .expect("Failed to get initial configuration"); - - assert!( - !initial_config.configurations.is_empty(), - "Should have initial configurations" - ); - - // Step 2: Update configuration - let mut updated_config = initial_config.configurations[0].clone(); - updated_config.value = "999999".to_string(); // Update to new value - - let update_request = UpdateConfigurationRequest { - configurations: vec![updated_config.clone()], - validate_before_update: true, - }; - - let update_response = trading_client - .update_configuration(update_request) - .await - .expect("Failed to update configuration"); - - assert!( - update_response.success, - "Configuration update should succeed" - ); - assert!( - update_response.validation_errors.is_empty(), - "Should have no validation errors" - ); - - // Step 3: Trigger hot reload - let reload_request = TriggerConfigReloadRequest { - services: vec!["trading_service".to_string(), "risk_service".to_string()], - config_types: vec![ConfigurationType::TradingLimits as i32], - }; - - let reload_response = trading_client - .trigger_config_reload(reload_request) - .await - .expect("Failed to trigger config reload"); - - assert!(reload_response.success, "Config reload should succeed"); - - // Verify all services reloaded successfully - for service_result in &reload_response.service_results { - assert!( - service_result.success, - "Service {} should reload successfully", - service_result.service_name - ); - } - - // Step 4: Verify configuration took effect - sleep(Duration::from_millis(500)).await; // Allow time for reload - - let final_config = trading_client - .get_configuration(get_config_request) - .await - .expect("Failed to get final configuration"); - - let updated_value = final_config - .configurations - .iter() - .find(|c| c.id == updated_config.id) - .expect("Should find updated configuration"); - - assert_eq!( - updated_value.value, "999999", - "Configuration should be updated" - ); - - // Step 5: Verify configuration history - let history_request = GetConfigurationHistoryRequest { - config_id: updated_config.id.clone(), - limit: Some(10), - }; - - let history_response = trading_client - .get_configuration_history(history_request) - .await - .expect("Failed to get configuration history"); - - assert!( - history_response.history.len() >= 2, - "Should have at least 2 history entries" - ); - - test_env - .teardown() - .await - .expect("Failed to teardown test environment"); - } - - #[tokio::test] - async fn test_configuration_validation_workflow() { - let mut test_env = EndToEndTestEnvironment::new(TestConfig::default()); - test_env - .setup() - .await - .expect("Failed to setup test environment"); - - let trading_client = test_env.get_trading_client(); - - // Test configuration validation with invalid values - let invalid_config = Configuration { - id: Uuid::new_v4().to_string(), - config_type: ConfigurationType::TradingLimits as i32, - key: "max_position_size".to_string(), - value: "-1000".to_string(), // Invalid negative value - account_id: Some("test_account".to_string()), - last_updated: Utc::now().timestamp(), - }; - - let validation_request = ValidateConfigurationRequest { - configurations: vec![invalid_config.clone()], - }; - - let validation_response = trading_client - .validate_configuration(validation_request) - .await - .expect("Failed to validate configuration"); - - assert!( - !validation_response.is_valid, - "Invalid configuration should fail validation" - ); - assert!( - !validation_response.validation_errors.is_empty(), - "Should have validation errors" - ); - - // Test with valid configuration - let valid_config = Configuration { - id: Uuid::new_v4().to_string(), - config_type: ConfigurationType::TradingLimits as i32, - key: "max_position_size".to_string(), - value: "1000000".to_string(), - account_id: Some("test_account".to_string()), - last_updated: Utc::now().timestamp(), - }; - - let valid_validation_request = ValidateConfigurationRequest { - configurations: vec![valid_config.clone()], - }; - - let valid_validation_response = trading_client - .validate_configuration(valid_validation_request) - .await - .expect("Failed to validate valid configuration"); - - assert!( - valid_validation_response.is_valid, - "Valid configuration should pass validation" - ); - assert!( - valid_validation_response.validation_errors.is_empty(), - "Should have no validation errors" - ); - - test_env - .teardown() - .await - .expect("Failed to teardown test environment"); - } -} - -/// Security authentication and authorization flow tests - DISABLED (TLI is pure client) -#[cfg(disabled_auth_tests)] -mod security_flow_tests { - use super::*; - - #[tokio::test] - async fn test_authentication_and_authorization_flow() { - let mut test_env = EndToEndTestEnvironment::new(TestConfig::default()); - test_env - .setup() - .await - .expect("Failed to setup test environment"); - - let auth_manager = test_env - .auth_manager - .as_ref() - .expect("Authentication manager not available"); - - // Step 1: User registration/creation - let user_credentials = UserCredentials { - username: "test_trader".to_string(), - password: "SecurePassword123!".to_string(), - email: Some("test.trader@example.com".to_string()), - full_name: Some("Test Trader".to_string()), - }; - - let registration_result = auth_manager - .register_user(user_credentials.clone()) - .await - .expect("Failed to register user"); - - assert!( - registration_result.success, - "User registration should succeed" - ); - let user_id = registration_result.user_id.expect("Should have user ID"); - - // Step 2: Authentication - let login_request = LoginRequest { - username: user_credentials.username.clone(), - password: user_credentials.password.clone(), - mfa_token: None, - }; - - let login_response = auth_manager - .authenticate(login_request) - .await - .expect("Failed to authenticate user"); - - assert!(login_response.success, "Authentication should succeed"); - assert!( - login_response.access_token.is_some(), - "Should have access token" - ); - assert!( - login_response.refresh_token.is_some(), - "Should have refresh token" - ); - - let access_token = login_response.access_token.unwrap(); - - // Step 3: Authorization check - let auth_request = AuthorizationRequest { - access_token: access_token.clone(), - resource: "trading_service".to_string(), - action: "submit_order".to_string(), - }; - - let auth_response = auth_manager - .authorize(auth_request) - .await - .expect("Failed to check authorization"); - - assert!( - auth_response.authorized, - "User should be authorized for trading" - ); - - // Step 4: Session management - let session_manager = test_env - .session_manager - .as_ref() - .expect("Session manager not available"); - - let session_info = session_manager - .get_session_info(&access_token) - .await - .expect("Failed to get session info"); - - assert_eq!( - session_info.user_id, user_id, - "Session should belong to correct user" - ); - assert!(session_info.is_active, "Session should be active"); - - // Step 5: Token refresh - let refresh_token = login_response.refresh_token.unwrap(); - let refresh_request = RefreshTokenRequest { - refresh_token: refresh_token.clone(), - }; - - let refresh_response = auth_manager - .refresh_token(refresh_request) - .await - .expect("Failed to refresh token"); - - assert!(refresh_response.success, "Token refresh should succeed"); - assert!( - refresh_response.access_token.is_some(), - "Should have new access token" - ); - - // Step 6: Logout - let logout_request = LogoutRequest { - access_token: access_token.clone(), - }; - - let logout_response = auth_manager - .logout(logout_request) - .await - .expect("Failed to logout"); - - assert!(logout_response.success, "Logout should succeed"); - - // Verify session is invalidated - let post_logout_session = session_manager.get_session_info(&access_token).await; - assert!( - post_logout_session.is_err() || !post_logout_session.unwrap().is_active, - "Session should be invalidated after logout" - ); - - test_env - .teardown() - .await - .expect("Failed to teardown test environment"); - } - - #[tokio::test] - async fn test_role_based_access_control() { - let mut test_env = EndToEndTestEnvironment::new(TestConfig::default()); - test_env - .setup() - .await - .expect("Failed to setup test environment"); - - let auth_manager = test_env - .auth_manager - .as_ref() - .expect("Authentication manager not available"); - - // Create users with different roles - let trader_credentials = UserCredentials { - username: "trader_user".to_string(), - password: "TraderPass123!".to_string(), - email: Some("trader@example.com".to_string()), - full_name: Some("Trader User".to_string()), - }; - - let admin_credentials = UserCredentials { - username: "admin_user".to_string(), - password: "AdminPass123!".to_string(), - email: Some("admin@example.com".to_string()), - full_name: Some("Admin User".to_string()), - }; - - // Register users - let trader_registration = auth_manager - .register_user(trader_credentials.clone()) - .await - .expect("Failed to register trader"); - let admin_registration = auth_manager - .register_user(admin_credentials.clone()) - .await - .expect("Failed to register admin"); - - let trader_user_id = trader_registration.user_id.unwrap(); - let admin_user_id = admin_registration.user_id.unwrap(); - - // Assign roles - let rbac_manager = RbacManager::new()?; - - rbac_manager - .assign_role_to_user(trader_user_id.clone(), "trader".to_string()) - .await - .expect("Failed to assign trader role"); - - rbac_manager - .assign_role_to_user(admin_user_id.clone(), "admin".to_string()) - .await - .expect("Failed to assign admin role"); - - // Test trader permissions - let trader_login = auth_manager - .authenticate(LoginRequest { - username: trader_credentials.username, - password: trader_credentials.password, - mfa_token: None, - }) - .await - .expect("Failed to login trader"); - - let trader_token = trader_login.access_token.unwrap(); - - // Trader should have trading permissions - let trading_auth = auth_manager - .authorize(AuthorizationRequest { - access_token: trader_token.clone(), - resource: "trading_service".to_string(), - action: "submit_order".to_string(), - }) - .await - .expect("Failed to check trading authorization"); - - assert!( - trading_auth.authorized, - "Trader should have trading permissions" - ); - - // Trader should NOT have admin permissions - let admin_auth = auth_manager - .authorize(AuthorizationRequest { - access_token: trader_token, - resource: "admin_service".to_string(), - action: "modify_user".to_string(), - }) - .await - .expect("Failed to check admin authorization"); - - assert!( - !admin_auth.authorized, - "Trader should NOT have admin permissions" - ); - - // Test admin permissions - let admin_login = auth_manager - .authenticate(LoginRequest { - username: admin_credentials.username, - password: admin_credentials.password, - mfa_token: None, - }) - .await - .expect("Failed to login admin"); - - let admin_token = admin_login.access_token.unwrap(); - - // Admin should have admin permissions - let admin_admin_auth = auth_manager - .authorize(AuthorizationRequest { - access_token: admin_token.clone(), - resource: "admin_service".to_string(), - action: "modify_user".to_string(), - }) - .await - .expect("Failed to check admin admin authorization"); - - assert!( - admin_admin_auth.authorized, - "Admin should have admin permissions" - ); - - // Admin should also have trading permissions - let admin_trading_auth = auth_manager - .authorize(AuthorizationRequest { - access_token: admin_token, - resource: "trading_service".to_string(), - action: "submit_order".to_string(), - }) - .await - .expect("Failed to check admin trading authorization"); - - assert!( - admin_trading_auth.authorized, - "Admin should have trading permissions" - ); - - test_env - .teardown() - .await - .expect("Failed to teardown test environment"); - } -} - -/// Event storage and retrieval tests -#[cfg(test)] -mod event_storage_tests { - use super::*; - - #[tokio::test] - async fn test_event_lifecycle_and_retrieval() { - let mut test_env = EndToEndTestEnvironment::new(TestConfig::default()); - test_env - .setup() - .await - .expect("Failed to setup test environment"); - - let trading_client = test_env.get_trading_client(); - - // Subscribe to events to capture them - let subscription_request = EventSubscriptionRequest { - event_types: vec![ - EventType::OrderExecuted as i32, - EventType::PositionChanged as i32, - EventType::ConfigurationChanged as i32, - ], - filters: HashMap::new(), - }; - - let mut event_stream = trading_client - .subscribe_to_events(subscription_request) - .await - .expect("Failed to subscribe to events"); - - let events_start_time = Utc::now(); - - // Generate events by performing various operations - let order_request = SubmitOrderRequest { - symbol: TestUtilities::generate_test_symbol("AAPL"), - side: OrderSide::Buy as i32, - order_type: OrderType::Market as i32, - quantity: 100.0, - price: None, - stop_price: None, - time_in_force: "DAY".to_string(), - client_order_id: Uuid::new_v4().to_string(), - }; - - // Submit order to generate events - let _order_response = trading_client - .submit_order(order_request) - .await - .expect("Failed to submit order"); - - // Update configuration to generate config events - let config_update = Configuration { - id: Uuid::new_v4().to_string(), - config_type: ConfigurationType::TradingLimits as i32, - key: "test_event_config".to_string(), - value: "test_value".to_string(), - account_id: Some("test_account".to_string()), - last_updated: Utc::now().timestamp(), - }; - - let update_request = UpdateConfigurationRequest { - configurations: vec![config_update], - validate_before_update: true, - }; - - let _config_response = trading_client - .update_configuration(update_request) - .await - .expect("Failed to update configuration"); - - // Collect events from stream - let mut collected_events = Vec::new(); - let mut event_collection_attempts = 0; - let max_collection_attempts = 10; - - while event_collection_attempts < max_collection_attempts && collected_events.len() < 3 { - match timeout(Duration::from_millis(500), event_stream.recv()).await { - Ok(Some(event)) => { - tracing::info!( - "Collected event: {:?} for {}", - event.event_type, - event.event_id - ); - collected_events.push(event); - } - Ok(None) => break, - Err(_) => { - tracing::debug!( - "Event collection timeout on attempt {}", - event_collection_attempts + 1 - ); - } - } - event_collection_attempts += 1; - } - - assert!( - !collected_events.is_empty(), - "Should have collected some events" - ); - - // Test event retrieval by time range - let events_end_time = Utc::now(); - let time_range_request = GetEventsRequest { - start_time: Some(events_start_time.timestamp()), - end_time: Some(events_end_time.timestamp()), - event_types: vec![ - EventType::OrderExecuted as i32, - EventType::PositionChanged as i32, - EventType::ConfigurationChanged as i32, - ], - limit: Some(100), - offset: None, - }; - - let time_range_response = trading_client - .get_events(time_range_request) - .await - .expect("Failed to get events by time range"); - - assert!( - !time_range_response.events.is_empty(), - "Should have events in time range" - ); - - // Test event retrieval by type - let type_filter_request = GetEventsRequest { - start_time: Some(events_start_time.timestamp()), - end_time: Some(events_end_time.timestamp()), - event_types: vec![EventType::OrderExecuted as i32], - limit: Some(50), - offset: None, - }; - - let type_filter_response = trading_client - .get_events(type_filter_request) - .await - .expect("Failed to get events by type"); - - // All returned events should be of the requested type - for event in &type_filter_response.events { - assert_eq!( - event.event_type, - EventType::OrderExecuted as i32, - "All events should be of OrderExecuted type" - ); - } - - // Test event statistics - let stats_request = GetEventStatisticsRequest { - start_time: events_start_time.timestamp(), - end_time: events_end_time.timestamp(), - group_by_type: true, - group_by_service: true, - }; - - let stats_response = trading_client - .get_event_statistics(stats_request) - .await - .expect("Failed to get event statistics"); - - assert!( - stats_response.total_events > 0, - "Should have total event count" - ); - assert!( - !stats_response.events_by_type.is_empty(), - "Should have events grouped by type" - ); - - test_env - .teardown() - .await - .expect("Failed to teardown test environment"); - } -} diff --git a/bin/fxt/tests/integration/error_handling_tests.rs b/bin/fxt/tests/integration/error_handling_tests.rs deleted file mode 100644 index b1d5918f4..000000000 --- a/bin/fxt/tests/integration/error_handling_tests.rs +++ /dev/null @@ -1,1302 +0,0 @@ -//! Error handling and resilience integration tests for TLI system -//! -//! This module tests various error scenarios including service unavailability, -//! network timeouts, database connection failures, and invalid data handling. - -use std::sync::{Arc, Mutex}; -use std::time::Duration; -use tokio::time::{sleep, timeout}; -use uuid::Uuid; -use wiremock::{ - matchers::{method, path}, - Mock, MockServer, ResponseTemplate, -}; - -use crate::integration::{TestConfig, TestUtilities}; -use crate::mocks::grpc_server::{FailureMode, MockBacktestingServer, MockTradingServer}; -use fxt::client::{BacktestingClient, TliClientBuilder, TradingClient}; -use fxt::error::{ErrorCode, ErrorSeverity, TliError}; -use fxt::prelude::*; - -/// Error handling test suite -pub struct ErrorHandlingTests { - config: TestConfig, - trading_client: Option, - backtesting_client: Option, - mock_servers: Vec>, - wiremock_servers: Vec, -} - -impl ErrorHandlingTests { - pub fn new(config: TestConfig) -> Self { - Self { - config, - trading_client: None, - backtesting_client: None, - mock_servers: Vec::new(), - wiremock_servers: Vec::new(), - } - } - - /// Setup test environment with controllable failure modes - pub async fn setup(&mut self) -> TliResult<()> { - tracing::info!("Setting up error handling test environment"); - - // Start controllable mock servers - let mut trading_server = MockTradingServer::new(self.config.mock_server_port)?; - let trading_port = trading_server.start()?; - self.mock_servers.push(Box::new(trading_server)); - - let mut backtesting_server = - MockBacktestingServer::new(self.config.mock_server_port + 100)?; - let backtesting_port = backtesting_server.start()?; - self.mock_servers.push(Box::new(backtesting_server)); - - // Wait for services to be ready - sleep(Duration::from_millis(300)).await; - - // Create TLI client suite with retry configuration - let client_suite = TliClientBuilder::new() - .with_service_endpoint( - "trading_service".to_string(), - format!("http://localhost:{}", trading_port), - ) - .with_service_endpoint( - "backtesting_service".to_string(), - format!("http://localhost:{}", backtesting_port), - ) - .with_trading_config(create_resilient_trading_config()) - .with_backtesting_config(create_resilient_backtesting_config()) - .build() - .await?; - - self.trading_client = client_suite.trading_client; - self.backtesting_client = client_suite.backtesting_client; - - tracing::info!("Error handling test environment setup complete"); - Ok(()) - } - - /// Setup test environment for network failure scenarios - pub async fn setup_with_network_failures(&mut self) -> TliResult<()> { - tracing::info!("Setting up network failure test environment"); - - // Start WireMock servers to simulate network issues - let trading_mock = MockServer::start().await; - let backtesting_mock = MockServer::start().await; - - let trading_port = trading_mock.address().port(); - let backtesting_port = backtesting_mock.address().port(); - - self.wiremock_servers.push(trading_mock); - self.wiremock_servers.push(backtesting_mock); - - // Create clients pointing to WireMock servers - let client_suite = TliClientBuilder::new() - .with_service_endpoint( - "trading_service".to_string(), - format!("http://localhost:{}", trading_port), - ) - .with_service_endpoint( - "backtesting_service".to_string(), - format!("http://localhost:{}", backtesting_port), - ) - .with_trading_config(create_resilient_trading_config()) - .with_backtesting_config(create_resilient_backtesting_config()) - .build() - .await?; - - self.trading_client = client_suite.trading_client; - self.backtesting_client = client_suite.backtesting_client; - - tracing::info!("Network failure test environment setup complete"); - Ok(()) - } - - /// Cleanup test environment - pub async fn teardown(&mut self) -> TliResult<()> { - tracing::info!("Tearing down error handling test environment"); - - // Shutdown clients - if let Some(client) = self.trading_client.take() { - client.shutdown().await; - } - if let Some(client) = self.backtesting_client.take() { - client.shutdown().await; - } - - // Stop mock servers - for server in &mut self.mock_servers { - let _ = server.stop(); - } - self.mock_servers.clear(); - - // WireMock servers are automatically cleaned up when dropped - self.wiremock_servers.clear(); - - tracing::info!("Error handling test environment teardown complete"); - Ok(()) - } - - /// Configure mock server to simulate specific failure mode - pub async fn configure_failure_mode( - &mut self, - service: &str, - failure_mode: FailureMode, - ) -> TliResult<()> { - for server in &mut self.mock_servers { - if server.get_service_name() == service { - server.set_failure_mode(failure_mode)?; - break; - } - } - Ok(()) - } -} - -fn create_resilient_trading_config() -> TradingClientConfig { - TradingClientConfig { - service_name: "trading_service".to_string(), - request_timeout: Duration::from_secs(5), - order_validation: OrderValidationConfig { - enable_pre_trade_checks: true, - max_order_value: 1000000.0, - require_confirmation: false, - }, - risk_management: RiskManagementConfig { - enable_position_limits: true, - max_position_size: 10000.0, - max_daily_loss: 50000.0, - }, - market_data: MarketDataConfig { - subscription_timeout: Duration::from_secs(30), - reconnect_interval: Duration::from_secs(5), - max_reconnect_attempts: 5, - }, - monitoring: MonitoringConfig { - enable_health_checks: true, - health_check_interval: Duration::from_secs(10), - enable_circuit_breaker: true, - circuit_breaker_threshold: 5, - }, - event_streaming: EventStreamConfig { - buffer_size: 1000, - reconnect_policy: ReconnectPolicy::ExponentialBackoff, - max_reconnect_attempts: 10, - }, - } -} - -fn create_resilient_backtesting_config() -> BacktestingClientConfig { - BacktestingClientConfig { - service_name: "backtesting_service".to_string(), - request_timeout: Duration::from_secs(30), - long_running_timeout: Duration::from_secs(300), - retry_config: RetryConfig { - max_attempts: 3, - initial_delay: Duration::from_millis(100), - max_delay: Duration::from_secs(10), - backoff_multiplier: 2.0, - }, - } -} - -/// Service unavailability tests -#[cfg(test)] -mod service_unavailability_tests { - use super::*; - - #[tokio::test] - async fn test_trading_service_unavailable() { - let mut test_env = ErrorHandlingTests::new(TestConfig::default()); - test_env - .setup() - .await - .expect("Failed to setup test environment"); - - // Configure trading service to be unavailable - test_env - .configure_failure_mode("trading_service", FailureMode::ServiceUnavailable) - .await - .expect("Failed to configure failure mode"); - - let trading_client = test_env - .trading_client - .as_ref() - .expect("Trading client not available"); - - // Attempt order submission - let order_request = SubmitOrderRequest { - symbol: TestUtilities::generate_test_symbol("AAPL"), - side: OrderSide::Buy as i32, - order_type: OrderType::Market as i32, - quantity: 100.0, - price: Some(150.0), - stop_price: None, - time_in_force: "DAY".to_string(), - client_order_id: Uuid::new_v4().to_string(), - }; - - let result = trading_client.submit_order(order_request).await; - - assert!( - result.is_err(), - "Order submission should fail when service unavailable" - ); - - let error = result.unwrap_err(); - match error { - TliError::ServiceUnavailable(_) => { - tracing::info!("Correctly identified service unavailable error"); - } - _ => panic!("Expected ServiceUnavailable error, got: {:?}", error), - } - - test_env - .teardown() - .await - .expect("Failed to teardown test environment"); - } - - #[tokio::test] - async fn test_service_recovery_after_failure() { - let mut test_env = ErrorHandlingTests::new(TestConfig::default()); - test_env - .setup() - .await - .expect("Failed to setup test environment"); - - let trading_client = test_env - .trading_client - .as_ref() - .expect("Trading client not available"); - - // First, make service unavailable - test_env - .configure_failure_mode("trading_service", FailureMode::ServiceUnavailable) - .await - .expect("Failed to configure failure mode"); - - let order_request = SubmitOrderRequest { - symbol: TestUtilities::generate_test_symbol("AAPL"), - side: OrderSide::Buy as i32, - order_type: OrderType::Market as i32, - quantity: 100.0, - price: Some(150.0), - stop_price: None, - time_in_force: "DAY".to_string(), - client_order_id: Uuid::new_v4().to_string(), - }; - - // Verify service is failing - let result = trading_client.submit_order(order_request.clone()).await; - assert!(result.is_err(), "Service should be failing"); - - // Restore service - test_env - .configure_failure_mode("trading_service", FailureMode::None) - .await - .expect("Failed to restore service"); - - // Wait for circuit breaker to reset - sleep(Duration::from_secs(1)).await; - - // Verify service recovery - let recovery_result = trading_client.submit_order(order_request).await; - assert!( - recovery_result.is_ok(), - "Service should recover after restoration" - ); - - test_env - .teardown() - .await - .expect("Failed to teardown test environment"); - } - - #[tokio::test] - async fn test_partial_service_failure() { - let mut test_env = ErrorHandlingTests::new(TestConfig::default()); - test_env - .setup() - .await - .expect("Failed to setup test environment"); - - // Configure only backtesting service to fail - test_env - .configure_failure_mode("backtesting_service", FailureMode::ServiceUnavailable) - .await - .expect("Failed to configure failure mode"); - - let trading_client = test_env - .trading_client - .as_ref() - .expect("Trading client not available"); - let backtesting_client = test_env - .backtesting_client - .as_ref() - .expect("Backtesting client not available"); - - // Trading service should still work - let order_request = SubmitOrderRequest { - symbol: TestUtilities::generate_test_symbol("AAPL"), - side: OrderSide::Buy as i32, - order_type: OrderType::Market as i32, - quantity: 100.0, - price: Some(150.0), - stop_price: None, - time_in_force: "DAY".to_string(), - client_order_id: Uuid::new_v4().to_string(), - }; - - let trading_result = trading_client.submit_order(order_request).await; - assert!(trading_result.is_ok(), "Trading service should still work"); - - // Backtesting service should fail - let backtest_request = CreateBacktestRequest { - name: "Test Backtest".to_string(), - strategy_id: "test_strategy".to_string(), - start_date: "2024-01-01".to_string(), - end_date: "2024-01-31".to_string(), - initial_capital: 100000.0, - symbols: vec!["AAPL".to_string()], - parameters: HashMap::new(), - }; - - let backtest_result = backtesting_client.create_backtest(backtest_request).await; - assert!(backtest_result.is_err(), "Backtesting service should fail"); - - test_env - .teardown() - .await - .expect("Failed to teardown test environment"); - } -} - -/// Network timeout and connection tests -#[cfg(test)] -mod network_timeout_tests { - use super::*; - - #[tokio::test] - async fn test_connection_timeout_handling() { - let mut test_env = ErrorHandlingTests::new(TestConfig::default()); - test_env - .setup_with_network_failures() - .await - .expect("Failed to setup test environment"); - - // Configure mock to not respond (simulate timeout) - let trading_mock = &test_env.wiremock_servers[0]; - Mock::given(method("POST")) - .and(path("/trading.TradingService/SubmitOrder")) - .respond_with(ResponseTemplate::new(200).set_delay(Duration::from_secs(10))) // Longer than client timeout - .mount(trading_mock) - .await; - - let trading_client = test_env - .trading_client - .as_ref() - .expect("Trading client not available"); - - let order_request = SubmitOrderRequest { - symbol: TestUtilities::generate_test_symbol("AAPL"), - side: OrderSide::Buy as i32, - order_type: OrderType::Market as i32, - quantity: 100.0, - price: Some(150.0), - stop_price: None, - time_in_force: "DAY".to_string(), - client_order_id: Uuid::new_v4().to_string(), - }; - - let start_time = std::time::Instant::now(); - let result = trading_client.submit_order(order_request).await; - let elapsed = start_time.elapsed(); - - assert!(result.is_err(), "Request should timeout"); - assert!( - elapsed < Duration::from_secs(8), - "Should timeout within client timeout period" - ); - - let error = result.unwrap_err(); - match error { - TliError::Timeout(_) => { - tracing::info!("Correctly identified timeout error"); - } - _ => panic!("Expected Timeout error, got: {:?}", error), - } - - test_env - .teardown() - .await - .expect("Failed to teardown test environment"); - } - - #[tokio::test] - async fn test_connection_refused_handling() { - let mut test_env = ErrorHandlingTests::new(TestConfig::default()); - - // Create clients pointing to non-existent services - let client_suite = TliClientBuilder::new() - .with_service_endpoint( - "trading_service".to_string(), - "http://localhost:99999".to_string(), - ) // Invalid port - .with_service_endpoint( - "backtesting_service".to_string(), - "http://localhost:99998".to_string(), - ) // Invalid port - .with_trading_config(create_resilient_trading_config()) - .with_backtesting_config(create_resilient_backtesting_config()) - .build() - .await - .expect("Client creation should succeed"); - - test_env.trading_client = client_suite.trading_client; - - let trading_client = test_env - .trading_client - .as_ref() - .expect("Trading client not available"); - - let order_request = SubmitOrderRequest { - symbol: TestUtilities::generate_test_symbol("AAPL"), - side: OrderSide::Buy as i32, - order_type: OrderType::Market as i32, - quantity: 100.0, - price: Some(150.0), - stop_price: None, - time_in_force: "DAY".to_string(), - client_order_id: Uuid::new_v4().to_string(), - }; - - let result = trading_client.submit_order(order_request).await; - - assert!(result.is_err(), "Connection should be refused"); - - let error = result.unwrap_err(); - match error { - TliError::Connection(_) => { - tracing::info!("Correctly identified connection error"); - } - _ => panic!("Expected Connection error, got: {:?}", error), - } - - test_env - .teardown() - .await - .expect("Failed to teardown test environment"); - } - - #[tokio::test] - async fn test_intermittent_network_failures() { - let mut test_env = ErrorHandlingTests::new(TestConfig::default()); - test_env - .setup_with_network_failures() - .await - .expect("Failed to setup test environment"); - - let trading_mock = &test_env.wiremock_servers[0]; - - // Configure mock to fail 50% of the time - let success_counter = Arc::new(Mutex::new(0)); - let counter_clone = success_counter.clone(); - - Mock::given(method("POST")) - .and(path("/trading.TradingService/SubmitOrder")) - .respond_with(move |_req| { - let mut counter = counter_clone.lock().unwrap(); - *counter += 1; - if *counter % 2 == 0 { - ResponseTemplate::new(200).set_body_json(serde_json::json!({ - "success": true, - "order_id": "test_order_123", - "message": "Order submitted successfully", - "timestamp_unix_nanos": 1234567890000000000i64 - })) - } else { - ResponseTemplate::new(500).set_body("Internal Server Error") - } - }) - .mount(trading_mock) - .await; - - let trading_client = test_env - .trading_client - .as_ref() - .expect("Trading client not available"); - - // Attempt multiple requests - let mut successful_requests = 0; - let mut failed_requests = 0; - let total_requests = 10; - - for i in 0..total_requests { - let order_request = SubmitOrderRequest { - symbol: TestUtilities::generate_test_symbol("AAPL"), - side: OrderSide::Buy as i32, - order_type: OrderType::Market as i32, - quantity: 100.0, - price: Some(150.0), - stop_price: None, - time_in_force: "DAY".to_string(), - client_order_id: format!("order_{}", i), - }; - - match trading_client.submit_order(order_request).await { - Ok(_) => successful_requests += 1, - Err(_) => failed_requests += 1, - } - } - - tracing::info!( - "Intermittent failure test: {} successful, {} failed", - successful_requests, - failed_requests - ); - - // Should have both successes and failures - assert!( - successful_requests > 0, - "Should have some successful requests" - ); - assert!(failed_requests > 0, "Should have some failed requests"); - - test_env - .teardown() - .await - .expect("Failed to teardown test environment"); - } -} - -/// Database connection failure tests -#[cfg(test)] -mod database_failure_tests { - use super::*; - - #[tokio::test] - async fn test_database_connection_failure() { - let mut test_env = ErrorHandlingTests::new(TestConfig::default()); - test_env - .setup() - .await - .expect("Failed to setup test environment"); - - // Configure database failure - test_env - .configure_failure_mode("trading_service", FailureMode::DatabaseError) - .await - .expect("Failed to configure database failure"); - - let trading_client = test_env - .trading_client - .as_ref() - .expect("Trading client not available"); - - // Attempt to get positions (requires database access) - let position_request = GetPositionsRequest { - account_id: Some("test_account".to_string()), - symbol_filter: None, - include_zero_positions: false, - }; - - let result = trading_client.get_positions(position_request).await; - - assert!(result.is_err(), "Database operation should fail"); - - let error = result.unwrap_err(); - match error { - TliError::Database(_) => { - tracing::info!("Correctly identified database error"); - } - _ => panic!("Expected Database error, got: {:?}", error), - } - - test_env - .teardown() - .await - .expect("Failed to teardown test environment"); - } - - #[tokio::test] - async fn test_database_transaction_rollback() { - let mut test_env = ErrorHandlingTests::new(TestConfig::default()); - test_env - .setup() - .await - .expect("Failed to setup test environment"); - - // Configure intermittent database failures - test_env - .configure_failure_mode("trading_service", FailureMode::TransactionFailure) - .await - .expect("Failed to configure transaction failure"); - - let trading_client = test_env - .trading_client - .as_ref() - .expect("Trading client not available"); - - // Attempt batch order submission (requires transactions) - let batch_orders = vec![ - SubmitOrderRequest { - symbol: TestUtilities::generate_test_symbol("AAPL"), - side: OrderSide::Buy as i32, - order_type: OrderType::Market as i32, - quantity: 100.0, - price: Some(150.0), - stop_price: None, - time_in_force: "DAY".to_string(), - client_order_id: Uuid::new_v4().to_string(), - }, - SubmitOrderRequest { - symbol: TestUtilities::generate_test_symbol("GOOGL"), - side: OrderSide::Buy as i32, - order_type: OrderType::Market as i32, - quantity: 50.0, - price: Some(2800.0), - stop_price: None, - time_in_force: "DAY".to_string(), - client_order_id: Uuid::new_v4().to_string(), - }, - ]; - - let batch_request = SubmitBatchOrdersRequest { - orders: batch_orders, - all_or_none: true, // Requires transaction - max_acceptable_failures: 0, - }; - - let result = trading_client.submit_batch_orders(batch_request).await; - - assert!( - result.is_err(), - "Batch operation should fail due to transaction failure" - ); - - let error = result.unwrap_err(); - match error { - TliError::Database(_) | TliError::TransactionFailure(_) => { - tracing::info!("Correctly identified transaction failure"); - } - _ => panic!( - "Expected Database or TransactionFailure error, got: {:?}", - error - ), - } - - test_env - .teardown() - .await - .expect("Failed to teardown test environment"); - } - - #[tokio::test] - async fn test_database_recovery_mechanisms() { - let mut test_env = ErrorHandlingTests::new(TestConfig::default()); - test_env - .setup() - .await - .expect("Failed to setup test environment"); - - let trading_client = test_env - .trading_client - .as_ref() - .expect("Trading client not available"); - - // First, cause database failure - test_env - .configure_failure_mode("trading_service", FailureMode::DatabaseError) - .await - .expect("Failed to configure database failure"); - - let position_request = GetPositionsRequest { - account_id: Some("test_account".to_string()), - symbol_filter: None, - include_zero_positions: false, - }; - - // Verify failure - let result = trading_client.get_positions(position_request.clone()).await; - assert!(result.is_err(), "Database operation should fail"); - - // Restore database - test_env - .configure_failure_mode("trading_service", FailureMode::None) - .await - .expect("Failed to restore database"); - - // Wait for recovery - sleep(Duration::from_secs(1)).await; - - // Verify recovery - let recovery_result = trading_client.get_positions(position_request).await; - assert!( - recovery_result.is_ok(), - "Database operation should succeed after recovery" - ); - - test_env - .teardown() - .await - .expect("Failed to teardown test environment"); - } -} - -/// Invalid data handling tests -#[cfg(test)] -mod invalid_data_tests { - use super::*; - - #[tokio::test] - async fn test_invalid_order_data_handling() { - let mut test_env = ErrorHandlingTests::new(TestConfig::default()); - test_env - .setup() - .await - .expect("Failed to setup test environment"); - - let trading_client = test_env - .trading_client - .as_ref() - .expect("Trading client not available"); - - // Test invalid symbol - let invalid_symbol_request = SubmitOrderRequest { - symbol: "".to_string(), // Empty symbol - side: OrderSide::Buy as i32, - order_type: OrderType::Market as i32, - quantity: 100.0, - price: Some(150.0), - stop_price: None, - time_in_force: "DAY".to_string(), - client_order_id: Uuid::new_v4().to_string(), - }; - - let result = trading_client.submit_order(invalid_symbol_request).await; - assert!(result.is_err(), "Order with empty symbol should fail"); - - let error = result.unwrap_err(); - match error { - TliError::InvalidInput(_) | TliError::Validation(_) => { - tracing::info!("Correctly identified invalid symbol error"); - } - _ => panic!( - "Expected InvalidInput or Validation error, got: {:?}", - error - ), - } - - // Test invalid quantity - let invalid_quantity_request = SubmitOrderRequest { - symbol: TestUtilities::generate_test_symbol("AAPL"), - side: OrderSide::Buy as i32, - order_type: OrderType::Market as i32, - quantity: -100.0, // Negative quantity - price: Some(150.0), - stop_price: None, - time_in_force: "DAY".to_string(), - client_order_id: Uuid::new_v4().to_string(), - }; - - let result = trading_client.submit_order(invalid_quantity_request).await; - assert!(result.is_err(), "Order with negative quantity should fail"); - - // Test invalid price - let invalid_price_request = SubmitOrderRequest { - symbol: TestUtilities::generate_test_symbol("AAPL"), - side: OrderSide::Buy as i32, - order_type: OrderType::Limit as i32, - quantity: 100.0, - price: Some(-10.0), // Negative price - stop_price: None, - time_in_force: "DAY".to_string(), - client_order_id: Uuid::new_v4().to_string(), - }; - - let result = trading_client.submit_order(invalid_price_request).await; - assert!(result.is_err(), "Order with negative price should fail"); - - test_env - .teardown() - .await - .expect("Failed to teardown test environment"); - } - - #[tokio::test] - async fn test_malformed_configuration_data() { - let mut test_env = ErrorHandlingTests::new(TestConfig::default()); - test_env - .setup() - .await - .expect("Failed to setup test environment"); - - let trading_client = test_env - .trading_client - .as_ref() - .expect("Trading client not available"); - - // Test invalid configuration values - let invalid_configs = vec![ - Configuration { - id: Uuid::new_v4().to_string(), - config_type: ConfigurationType::TradingLimits as i32, - key: "max_position_size".to_string(), - value: "not_a_number".to_string(), // Invalid numeric value - account_id: Some("test_account".to_string()), - last_updated: chrono::Utc::now().timestamp(), - }, - Configuration { - id: Uuid::new_v4().to_string(), - config_type: ConfigurationType::TradingLimits as i32, - key: "".to_string(), // Empty key - value: "1000".to_string(), - account_id: Some("test_account".to_string()), - last_updated: chrono::Utc::now().timestamp(), - }, - ]; - - let update_request = UpdateConfigurationRequest { - configurations: invalid_configs, - validate_before_update: true, - }; - - let result = trading_client.update_configuration(update_request).await; - - // Should either fail validation or return validation errors - match result { - Ok(response) => { - assert!( - !response.success || !response.validation_errors.is_empty(), - "Invalid configuration should fail validation" - ); - } - Err(error) => match error { - TliError::InvalidInput(_) | TliError::Validation(_) => { - tracing::info!("Correctly identified invalid configuration error"); - } - _ => panic!( - "Expected InvalidInput or Validation error, got: {:?}", - error - ), - }, - } - - test_env - .teardown() - .await - .expect("Failed to teardown test environment"); - } - - #[tokio::test] - async fn test_corrupted_backtest_data() { - let mut test_env = ErrorHandlingTests::new(TestConfig::default()); - test_env - .setup() - .await - .expect("Failed to setup test environment"); - - let backtesting_client = test_env - .backtesting_client - .as_ref() - .expect("Backtesting client not available"); - - // Test invalid date format - let invalid_date_request = CreateBacktestRequest { - name: "Invalid Date Test".to_string(), - strategy_id: "test_strategy".to_string(), - start_date: "invalid-date-format".to_string(), // Invalid date - end_date: "2024-01-31".to_string(), - initial_capital: 100000.0, - symbols: vec!["AAPL".to_string()], - parameters: HashMap::new(), - }; - - let result = backtesting_client - .create_backtest(invalid_date_request) - .await; - assert!(result.is_err(), "Backtest with invalid date should fail"); - - // Test invalid initial capital - let invalid_capital_request = CreateBacktestRequest { - name: "Invalid Capital Test".to_string(), - strategy_id: "test_strategy".to_string(), - start_date: "2024-01-01".to_string(), - end_date: "2024-01-31".to_string(), - initial_capital: -1000.0, // Negative capital - symbols: vec!["AAPL".to_string()], - parameters: HashMap::new(), - }; - - let result = backtesting_client - .create_backtest(invalid_capital_request) - .await; - assert!( - result.is_err(), - "Backtest with negative capital should fail" - ); - - // Test empty symbols list - let empty_symbols_request = CreateBacktestRequest { - name: "Empty Symbols Test".to_string(), - strategy_id: "test_strategy".to_string(), - start_date: "2024-01-01".to_string(), - end_date: "2024-01-31".to_string(), - initial_capital: 100000.0, - symbols: vec![], // Empty symbols - parameters: HashMap::new(), - }; - - let result = backtesting_client - .create_backtest(empty_symbols_request) - .await; - assert!(result.is_err(), "Backtest with empty symbols should fail"); - - test_env - .teardown() - .await - .expect("Failed to teardown test environment"); - } -} - -/// Circuit breaker and rate limiting tests -#[cfg(test)] -mod resilience_pattern_tests { - use super::*; - - #[tokio::test] - async fn test_circuit_breaker_activation() { - let mut test_env = ErrorHandlingTests::new(TestConfig::default()); - test_env - .setup() - .await - .expect("Failed to setup test environment"); - - // Configure service to fail consistently - test_env - .configure_failure_mode("trading_service", FailureMode::InternalError) - .await - .expect("Failed to configure failure mode"); - - let trading_client = test_env - .trading_client - .as_ref() - .expect("Trading client not available"); - - let order_request = SubmitOrderRequest { - symbol: TestUtilities::generate_test_symbol("AAPL"), - side: OrderSide::Buy as i32, - order_type: OrderType::Market as i32, - quantity: 100.0, - price: Some(150.0), - stop_price: None, - time_in_force: "DAY".to_string(), - client_order_id: Uuid::new_v4().to_string(), - }; - - // Make multiple requests to trigger circuit breaker - let mut consecutive_failures = 0; - let max_attempts = 10; - - for attempt in 1..=max_attempts { - let result = trading_client.submit_order(order_request.clone()).await; - - if result.is_err() { - consecutive_failures += 1; - tracing::info!("Attempt {}: Request failed", attempt); - - // Check if we're getting circuit breaker errors - let error = result.unwrap_err(); - match error { - TliError::CircuitBreakerOpen(_) => { - tracing::info!( - "Circuit breaker activated after {} failures", - consecutive_failures - ); - break; - } - _ => { - // Continue with other types of errors - } - } - } else { - consecutive_failures = 0; - } - - sleep(Duration::from_millis(100)).await; - } - - assert!( - consecutive_failures >= 3, - "Should have multiple consecutive failures" - ); - - test_env - .teardown() - .await - .expect("Failed to teardown test environment"); - } - - #[tokio::test] - async fn test_retry_with_exponential_backoff() { - let mut test_env = ErrorHandlingTests::new(TestConfig::default()); - test_env - .setup() - .await - .expect("Failed to setup test environment"); - - // Configure intermittent failures - test_env - .configure_failure_mode("backtesting_service", FailureMode::IntermittentFailure) - .await - .expect("Failed to configure failure mode"); - - let backtesting_client = test_env - .backtesting_client - .as_ref() - .expect("Backtesting client not available"); - - let backtest_request = CreateBacktestRequest { - name: "Retry Test Backtest".to_string(), - strategy_id: "test_strategy".to_string(), - start_date: "2024-01-01".to_string(), - end_date: "2024-01-31".to_string(), - initial_capital: 100000.0, - symbols: vec!["AAPL".to_string()], - parameters: HashMap::new(), - }; - - let start_time = std::time::Instant::now(); - let result = backtesting_client.create_backtest(backtest_request).await; - let elapsed = start_time.elapsed(); - - // The client should eventually succeed due to retry logic - // or fail after exhausting retries - match result { - Ok(_) => { - tracing::info!("Request succeeded after retries in {:?}", elapsed); - assert!( - elapsed > Duration::from_millis(100), - "Should have taken some time due to retries" - ); - } - Err(error) => { - tracing::info!("Request failed after retries: {:?}", error); - assert!( - elapsed > Duration::from_millis(500), - "Should have taken time due to multiple retry attempts" - ); - } - } - - test_env - .teardown() - .await - .expect("Failed to teardown test environment"); - } - - #[tokio::test] - async fn test_rate_limiting_handling() { - let mut test_env = ErrorHandlingTests::new(TestConfig::default()); - test_env - .setup() - .await - .expect("Failed to setup test environment"); - - // Configure rate limiting - test_env - .configure_failure_mode("trading_service", FailureMode::RateLimited) - .await - .expect("Failed to configure rate limiting"); - - let trading_client = test_env - .trading_client - .as_ref() - .expect("Trading client not available"); - - // Rapidly submit multiple orders - let mut rate_limited_count = 0; - let total_requests = 20; - - for i in 0..total_requests { - let order_request = SubmitOrderRequest { - symbol: TestUtilities::generate_test_symbol("AAPL"), - side: OrderSide::Buy as i32, - order_type: OrderType::Market as i32, - quantity: 100.0, - price: Some(150.0), - stop_price: None, - time_in_force: "DAY".to_string(), - client_order_id: format!("rapid_order_{}", i), - }; - - let result = trading_client.submit_order(order_request).await; - - if let Err(error) = result { - match error { - TliError::RateLimited(_) => { - rate_limited_count += 1; - tracing::info!("Request {} rate limited", i); - } - _ => { - tracing::info!("Request {} failed with other error: {:?}", i, error); - } - } - } - - // Small delay between requests - sleep(Duration::from_millis(10)).await; - } - - assert!( - rate_limited_count > 0, - "Should have encountered rate limiting" - ); - tracing::info!( - "Rate limited {} out of {} requests", - rate_limited_count, - total_requests - ); - - test_env - .teardown() - .await - .expect("Failed to teardown test environment"); - } -} - -/// Graceful degradation tests -#[cfg(test)] -mod graceful_degradation_tests { - use super::*; - - #[tokio::test] - async fn test_fallback_to_cached_data() { - let mut test_env = ErrorHandlingTests::new(TestConfig::default()); - test_env - .setup() - .await - .expect("Failed to setup test environment"); - - let trading_client = test_env - .trading_client - .as_ref() - .expect("Trading client not available"); - - // First, get positions while service is working (populate cache) - let position_request = GetPositionsRequest { - account_id: Some("test_account".to_string()), - symbol_filter: None, - include_zero_positions: false, - }; - - let initial_result = trading_client.get_positions(position_request.clone()).await; - assert!(initial_result.is_ok(), "Initial request should succeed"); - - // Now configure service to fail - test_env - .configure_failure_mode("trading_service", FailureMode::ServiceUnavailable) - .await - .expect("Failed to configure failure mode"); - - // Subsequent request should fall back to cached data - let cached_result = trading_client.get_positions(position_request).await; - - match cached_result { - Ok(response) => { - // Should indicate data is from cache - assert!( - response.is_cached.unwrap_or(false), - "Response should indicate cached data" - ); - tracing::info!("Successfully fell back to cached data"); - } - Err(error) => { - // Some implementations might not have caching - tracing::info!("Cache fallback not available: {:?}", error); - } - } - - test_env - .teardown() - .await - .expect("Failed to teardown test environment"); - } - - #[tokio::test] - async fn test_reduced_functionality_mode() { - let mut test_env = ErrorHandlingTests::new(TestConfig::default()); - test_env - .setup() - .await - .expect("Failed to setup test environment"); - - // Configure partial service degradation - test_env - .configure_failure_mode("trading_service", FailureMode::PartialDegradation) - .await - .expect("Failed to configure degradation"); - - let trading_client = test_env - .trading_client - .as_ref() - .expect("Trading client not available"); - - // Basic order submission should still work - let order_request = SubmitOrderRequest { - symbol: TestUtilities::generate_test_symbol("AAPL"), - side: OrderSide::Buy as i32, - order_type: OrderType::Market as i32, - quantity: 100.0, - price: Some(150.0), - stop_price: None, - time_in_force: "DAY".to_string(), - client_order_id: Uuid::new_v4().to_string(), - }; - - let order_result = trading_client.submit_order(order_request).await; - assert!( - order_result.is_ok(), - "Basic order submission should work in degraded mode" - ); - - // Advanced features might be disabled - let analytics_request = PortfolioAnalyticsRequest { - account_id: "test_account".to_string(), - calculation_date: chrono::Utc::now().timestamp(), - include_realized_pnl: true, - include_unrealized_pnl: true, - include_risk_metrics: true, - }; - - let analytics_result = trading_client - .get_portfolio_analytics(analytics_request) - .await; - - match analytics_result { - Ok(response) => { - // Should indicate reduced functionality - assert!( - response.warnings.is_some(), - "Should have warnings about reduced functionality" - ); - } - Err(error) => { - // Advanced features disabled in degraded mode - match error { - TliError::FeatureUnavailable(_) => { - tracing::info!("Advanced features correctly disabled in degraded mode"); - } - _ => panic!("Unexpected error in degraded mode: {:?}", error), - } - } - } - - test_env - .teardown() - .await - .expect("Failed to teardown test environment"); - } -} diff --git a/bin/fxt/tests/integration/mod.rs b/bin/fxt/tests/integration/mod.rs index 1d4f8ec92..1286eb68f 100644 --- a/bin/fxt/tests/integration/mod.rs +++ b/bin/fxt/tests/integration/mod.rs @@ -1,3 +1,3 @@ //! Integration tests module - all integration tests disabled //! -//! Tests need refactoring after TLI client architecture changes +//! Tests need refactoring after FXT client architecture changes diff --git a/bin/fxt/tests/integration/performance_tests.rs b/bin/fxt/tests/integration/performance_tests.rs index 7c71807b2..6c9086b04 100644 --- a/bin/fxt/tests/integration/performance_tests.rs +++ b/bin/fxt/tests/integration/performance_tests.rs @@ -144,7 +144,7 @@ impl PerformanceTestEnvironment { } /// Setup high-performance test environment - pub async fn setup(&mut self) -> TliResult<()> { + pub async fn setup(&mut self) -> FxtResult<()> { tracing::info!("Setting up performance test environment"); // Start high-performance mock servers @@ -185,7 +185,7 @@ impl PerformanceTestEnvironment { } /// Cleanup test environment - pub async fn teardown(&mut self) -> TliResult<()> { + pub async fn teardown(&mut self) -> FxtResult<()> { tracing::info!("Tearing down performance test environment"); // Shutdown clients @@ -936,7 +936,7 @@ mod stress_tests { let success = match &result { Ok(_) => true, - Err(TliError::Connection(_)) => { + Err(FxtError::Connection(_)) => { error_counter.fetch_add(1, Ordering::Relaxed); false } diff --git a/bin/fxt/tests/integration/service_integration_tests.rs b/bin/fxt/tests/integration/service_integration_tests.rs index ffc2db442..3369ed4d8 100644 --- a/bin/fxt/tests/integration/service_integration_tests.rs +++ b/bin/fxt/tests/integration/service_integration_tests.rs @@ -31,8 +31,8 @@ pub struct ServiceIntegrationTests { /// Mock server trait for test servers pub trait MockServer: Send + Sync { - fn start(&mut self) -> TliResult; - fn stop(&mut self) -> TliResult<()>; + fn start(&mut self) -> FxtResult; + fn stop(&mut self) -> FxtResult<()>; fn is_running(&self) -> bool; fn get_port(&self) -> Option; } @@ -48,7 +48,7 @@ impl ServiceIntegrationTests { } /// Setup test environment with mock services - pub async fn setup(&mut self) -> TliResult<()> { + pub async fn setup(&mut self) -> FxtResult<()> { tracing::info!("Setting up service integration test environment"); // Start mock trading service @@ -88,7 +88,7 @@ impl ServiceIntegrationTests { } /// Cleanup test environment - pub async fn teardown(&mut self) -> TliResult<()> { + pub async fn teardown(&mut self) -> FxtResult<()> { tracing::info!("Tearing down service integration test environment"); // Shutdown clients diff --git a/bin/fxt/tests/ml_trading_commands_test.rs b/bin/fxt/tests/ml_trading_commands_test.rs index ee5599947..ed78436a3 100644 --- a/bin/fxt/tests/ml_trading_commands_test.rs +++ b/bin/fxt/tests/ml_trading_commands_test.rs @@ -142,7 +142,7 @@ mod test_auth { let encryption_key = hex::encode([42u8; 32]); // Simple deterministic key for tests std::env::set_var("FOXHUNT_ENCRYPTION_KEY", &encryption_key); - // Create temp directory structure: temp/foxhunt-tli/tokens/ + // Create temp directory structure: temp/foxhunt-fxt/tokens/ let temp_base = std::env::temp_dir().join(format!("foxhunt_fxt_config_{}", std::process::id())); let config_home = temp_base.clone(); diff --git a/proto/config_service.proto b/proto/config_service.proto deleted file mode 100644 index de96bb785..000000000 --- a/proto/config_service.proto +++ /dev/null @@ -1,81 +0,0 @@ -syntax = "proto3"; - -package foxhunt.config; - -// Configuration Management Service -service ConfigurationService { - // Get a single configuration value - rpc GetConfig(GetConfigRequest) returns (GetConfigResponse); - - // Update a configuration value - rpc UpdateConfig(UpdateConfigRequest) returns (UpdateConfigResponse); - - // List all configurations for a service scope - rpc ListConfigs(ListConfigsRequest) returns (ListConfigsResponse); - - // Trigger configuration reload - rpc ReloadConfig(ReloadConfigRequest) returns (ReloadConfigResponse); -} - -// Get configuration request -message GetConfigRequest { - string service_scope = 1; - string config_key = 2; -} - -// Get configuration response -message GetConfigResponse { - string config_value = 1; // JSON-serialized value - string data_type = 2; - string description = 3; - int64 updated_at = 4; // Unix timestamp - string updated_by = 5; -} - -// Update configuration request -message UpdateConfigRequest { - string service_scope = 1; - string config_key = 2; - string new_value = 3; // JSON-serialized value - string updated_by = 4; -} - -// Update configuration response -message UpdateConfigResponse { - bool success = 1; - string message = 2; -} - -// List configurations request -message ListConfigsRequest { - optional string service_scope = 1; // If not provided, lists all scopes -} - -// Configuration item -message ConfigItem { - string service_scope = 1; - string config_key = 2; - string config_value = 3; // JSON-serialized value - string data_type = 4; - string description = 5; - int64 created_at = 6; - int64 updated_at = 7; - string updated_by = 8; -} - -// List configurations response -message ListConfigsResponse { - repeated ConfigItem configs = 1; -} - -// Reload configuration request -message ReloadConfigRequest { - optional string service_scope = 1; - optional string config_key = 2; -} - -// Reload configuration response -message ReloadConfigResponse { - bool success = 1; - string message = 2; -} diff --git a/services/api_gateway/build.rs b/services/api_gateway/build.rs index dd2a3d0e8..b82355483 100644 --- a/services/api_gateway/build.rs +++ b/services/api_gateway/build.rs @@ -4,10 +4,9 @@ //! Proto packages: //! - `foxhunt.tli` (fxt_trading.proto) - Fat-client unified interface (server+client) //! - `trading` (trading.proto) - Backend trading service (client-only) -//! - `foxhunt.config` (config_service.proto) - Config service (server+client) //! - `risk` (risk.proto) - Risk service (client-only) //! - `monitoring` (monitoring.proto) - Monitoring service (server+client) -//! - `config` (config.proto) - Config backend (client-only) +//! - `config` (config.proto) - Config service (server+client) //! - `ml_training` (ml_training.proto) - ML Training service (server+client) //! - `trading_agent` (trading_agent.proto) - Trading Agent service (server+client) @@ -17,19 +16,6 @@ fn main() -> Result<(), Box> { let out_dir = std::path::PathBuf::from(std::env::var("OUT_DIR")?); - // Compile Config Service proto (foxhunt.config package) - config - .clone() - .build_server(true) - .build_client(true) - .file_descriptor_set_path(out_dir.join("config_service_descriptor.bin")) - .compile_well_known_types(true) - .extern_path(".google.protobuf", "::prost_types") - .type_attribute(".", "#[derive(serde::Serialize, serde::Deserialize)]") - .server_mod_attribute(".", "#[allow(unused_qualifications)]") - .client_mod_attribute(".", "#[allow(unused_qualifications)]") - .compile_protos(&["../../proto/config_service.proto"], &["../../proto"])?; - // Compile fat-client TLI proto (package: foxhunt.tli) — server+client // Contains TradingService, BacktestingService, and MLService // API Gateway acts as server for incoming client requests @@ -193,7 +179,6 @@ fn main() -> Result<(), Box> { &["../../proto"], )?; - println!("cargo:rerun-if-changed=../../proto/config_service.proto"); println!("cargo:rerun-if-changed=../../proto/fxt_trading.proto"); println!("cargo:rerun-if-changed=../../proto/trading.proto"); println!("cargo:rerun-if-changed=../../proto/risk.proto"); diff --git a/services/api_gateway/src/config/endpoints.rs b/services/api_gateway/src/config/endpoints.rs index 025befc60..6f95c525b 100644 --- a/services/api_gateway/src/config/endpoints.rs +++ b/services/api_gateway/src/config/endpoints.rs @@ -2,24 +2,35 @@ use crate::config::ConfigurationManager; use crate::error::GatewayConfigError; +use futures::Stream; +use std::pin::Pin; use tonic::{Request, Response, Status}; use tracing::{debug, info, instrument}; -// Import generated proto code from crate root -use crate::foxhunt::config_proto::{ - configuration_service_server::{ConfigurationService, ConfigurationServiceServer}, - ConfigItem as ProtoConfigItem, GetConfigRequest, GetConfigResponse, ListConfigsRequest, - ListConfigsResponse, ReloadConfigRequest, ReloadConfigResponse, UpdateConfigRequest, - UpdateConfigResponse, +use crate::config_backend::{ + config_service_server::{ConfigService, ConfigServiceServer}, + ConfigChangeEvent, ConfigDataType, ConfigurationCategory, ConfigurationSetting, + DeleteConfigurationRequest, DeleteConfigurationResponse, ExportConfigurationRequest, + ExportConfigurationResponse, GetConfigSchemaRequest, GetConfigSchemaResponse, + GetConfigurationHistoryRequest, GetConfigurationHistoryResponse, GetConfigurationRequest, + GetConfigurationResponse, ImportConfigurationRequest, ImportConfigurationResponse, + ListCategoriesRequest, ListCategoriesResponse, RollbackConfigurationRequest, + RollbackConfigurationResponse, StreamConfigChangesRequest, UpdateConfigSchemaRequest, + UpdateConfigSchemaResponse, UpdateConfigurationRequest, UpdateConfigurationResponse, + ValidateConfigurationRequest, ValidateConfigurationResponse, }; -/// gRPC service implementation for configuration management -pub struct ConfigurationServiceImpl { +/// gRPC service implementation for gateway-local configuration management. +/// +/// Implements the `config.ConfigService` trait from config.proto. +/// Handles GetConfiguration, UpdateConfiguration, and ListCategories locally; +/// all other RPCs return `Status::unimplemented`. +pub struct GatewayConfigService { manager: std::sync::Arc>, } -impl ConfigurationServiceImpl { - /// Creates a new ConfigurationServiceImpl +impl GatewayConfigService { + /// Creates a new GatewayConfigService pub fn new(manager: ConfigurationManager) -> Self { Self { manager: std::sync::Arc::new(tokio::sync::RwLock::new(manager)), @@ -27,63 +38,94 @@ impl ConfigurationServiceImpl { } /// Returns the gRPC server instance - pub fn into_server(self) -> ConfigurationServiceServer { - ConfigurationServiceServer::new(self) + pub fn into_server(self) -> ConfigServiceServer { + ConfigServiceServer::new(self) } } #[tonic::async_trait] -impl ConfigurationService for ConfigurationServiceImpl { +impl ConfigService for GatewayConfigService { + type StreamConfigChangesStream = + Pin> + Send>>; + #[instrument(skip_all)] - async fn get_config( + async fn get_configuration( &self, - request: Request, - ) -> Result, Status> { + request: Request, + ) -> Result, Status> { let req = request.into_inner(); - debug!("GetConfig: {}/{}", req.service_scope, req.config_key); + let category = req.category.unwrap_or_default(); + let key = req.key.unwrap_or_default(); + debug!("GetConfiguration: {}/{}", category, key); + + if category.is_empty() && key.is_empty() { + // List all configs + let manager = self.manager.read().await; + let configs = manager + .list_configs(None) + .await + .map_err(|e| Status::internal(format!("{}", e)))?; + + let settings: Result, Status> = configs + .into_iter() + .map(|c| config_to_setting(&c)) + .collect(); + + return Ok(Response::new(GetConfigurationResponse { + settings: settings?, + })); + } let manager = self.manager.read().await; - let config = manager - .get_config(&req.service_scope, &req.config_key) - .await - .map_err(|e| match e { - GatewayConfigError::NotFound { .. } => Status::not_found(format!("{}", e)), - _ => Status::internal(format!("{}", e)), - })?; - Ok(Response::new(GetConfigResponse { - config_value: serde_json::to_string(&config.config_value) - .map_err(|e| Status::internal(format!("Serialization error: {}", e)))?, - data_type: config.data_type, - description: config.description.unwrap_or_default(), - updated_at: config.updated_at.timestamp(), - updated_by: config.updated_by.unwrap_or_default(), - })) + if !key.is_empty() { + // Get specific config + let config = manager + .get_config(&category, &key) + .await + .map_err(|e| match e { + GatewayConfigError::NotFound { .. } => Status::not_found(format!("{}", e)), + _ => Status::internal(format!("{}", e)), + })?; + + Ok(Response::new(GetConfigurationResponse { + settings: vec![config_to_setting(&config)?], + })) + } else { + // List by category + let configs = manager + .list_configs(Some(&category)) + .await + .map_err(|e| Status::internal(format!("{}", e)))?; + + let settings: Result, Status> = configs + .into_iter() + .map(|c| config_to_setting(&c)) + .collect(); + + Ok(Response::new(GetConfigurationResponse { + settings: settings?, + })) + } } #[instrument(skip_all)] - async fn update_config( + async fn update_configuration( &self, - request: Request, - ) -> Result, Status> { + request: Request, + ) -> Result, Status> { let req = request.into_inner(); info!( - "UpdateConfig: {}/{} by {}", - req.service_scope, req.config_key, req.updated_by + "UpdateConfiguration: {}/{} by {}", + req.category, req.key, req.changed_by ); - // Parse new value as JSON - let new_value: serde_json::Value = serde_json::from_str(&req.new_value) + let new_value: serde_json::Value = serde_json::from_str(&req.value) .map_err(|e| Status::invalid_argument(format!("Invalid JSON: {}", e)))?; let manager = self.manager.read().await; manager - .update_config( - &req.service_scope, - &req.config_key, - new_value, - &req.updated_by, - ) + .update_config(&req.category, &req.key, new_value, &req.changed_by) .await .map_err(|e| match e { GatewayConfigError::Validation(msg) => Status::invalid_argument(msg), @@ -91,65 +133,159 @@ impl ConfigurationService for ConfigurationServiceImpl { _ => Status::internal(format!("{}", e)), })?; - Ok(Response::new(UpdateConfigResponse { + Ok(Response::new(UpdateConfigurationResponse { success: true, - message: format!( - "Successfully updated {}/{}", - req.service_scope, req.config_key - ), + message: format!("Successfully updated {}/{}", req.category, req.key), + validation_result: None, + timestamp: chrono::Utc::now().timestamp(), })) } #[instrument(skip_all)] - async fn list_configs( + async fn delete_configuration( &self, - request: Request, - ) -> Result, Status> { + _request: Request, + ) -> Result, Status> { + Err(Status::unimplemented( + "Delete not supported by gateway config", + )) + } + + #[instrument(skip_all)] + async fn list_categories( + &self, + request: Request, + ) -> Result, Status> { let req = request.into_inner(); - debug!("ListConfigs: {:?}", req.service_scope); + debug!("ListCategories: {:?}", req.parent_category); let manager = self.manager.read().await; let configs = manager - .list_configs(req.service_scope.as_deref()) + .list_configs(req.parent_category.as_deref()) .await .map_err(|e| Status::internal(format!("{}", e)))?; - let proto_configs: Result, Status> = configs - .into_iter() - .map(|c| { - Ok(ProtoConfigItem { - service_scope: c.service_scope, - config_key: c.config_key, - config_value: serde_json::to_string(&c.config_value) - .map_err(|e| Status::internal(format!("{}", e)))?, - data_type: c.data_type, - description: c.description.unwrap_or_default(), - created_at: c.created_at.timestamp(), - updated_at: c.updated_at.timestamp(), - updated_by: c.updated_by.unwrap_or_default(), - }) + // Extract unique categories from configs + let mut seen = std::collections::HashSet::new(); + let categories = configs + .iter() + .filter_map(|c| { + if seen.insert(c.service_scope.clone()) { + Some(ConfigurationCategory { + id: 0, + name: c.service_scope.clone(), + description: String::new(), + parent_id: None, + display_order: 0, + icon: None, + created_at: c.created_at.timestamp(), + children: vec![], + }) + } else { + None + } }) .collect(); - Ok(Response::new(ListConfigsResponse { - configs: proto_configs?, - })) + Ok(Response::new(ListCategoriesResponse { categories })) } - #[instrument(skip_all)] - async fn reload_config( + async fn stream_config_changes( &self, - request: Request, - ) -> Result, Status> { - let req = request.into_inner(); - info!("ReloadConfig: {:?}/{:?}", req.service_scope, req.config_key); + _request: Request, + ) -> Result, Status> { + Err(Status::unimplemented( + "Streaming not supported by gateway config", + )) + } - // For now, this is a no-op since hot-reload is automatic via NOTIFY/LISTEN - // In the future, this could force a cache invalidation + async fn validate_configuration( + &self, + _request: Request, + ) -> Result, Status> { + Err(Status::unimplemented( + "Validation not supported by gateway config", + )) + } - Ok(Response::new(ReloadConfigResponse { - success: true, - message: "Configuration reload triggered (hot-reload active)".to_string(), - })) + async fn get_configuration_history( + &self, + _request: Request, + ) -> Result, Status> { + Err(Status::unimplemented( + "History not supported by gateway config", + )) + } + + async fn rollback_configuration( + &self, + _request: Request, + ) -> Result, Status> { + Err(Status::unimplemented( + "Rollback not supported by gateway config", + )) + } + + async fn export_configuration( + &self, + _request: Request, + ) -> Result, Status> { + Err(Status::unimplemented( + "Export not supported by gateway config", + )) + } + + async fn import_configuration( + &self, + _request: Request, + ) -> Result, Status> { + Err(Status::unimplemented( + "Import not supported by gateway config", + )) + } + + async fn get_config_schema( + &self, + _request: Request, + ) -> Result, Status> { + Err(Status::unimplemented( + "Schema not supported by gateway config", + )) + } + + async fn update_config_schema( + &self, + _request: Request, + ) -> Result, Status> { + Err(Status::unimplemented( + "Schema update not supported by gateway config", + )) } } + +/// Convert a ConfigItem (from ConfigurationManager) to a ConfigurationSetting proto message +fn config_to_setting(c: &crate::config::ConfigItem) -> Result { + Ok(ConfigurationSetting { + id: 0, + category: c.service_scope.clone(), + key: c.config_key.clone(), + value: serde_json::to_string(&c.config_value) + .map_err(|e| Status::internal(format!("Serialization error: {}", e)))?, + data_type: ConfigDataType::String as i32, + hot_reload: false, + description: c.description.clone().unwrap_or_default(), + default_value: None, + required: false, + sensitive: false, + validation_rule: None, + environment_override: None, + min_value: None, + max_value: None, + enum_values: None, + depends_on: vec![], + tags: vec![], + display_order: 0, + created_at: c.created_at.timestamp(), + modified_at: c.updated_at.timestamp(), + }) +} diff --git a/services/api_gateway/src/config/mod.rs b/services/api_gateway/src/config/mod.rs index d4b7dad15..4b5136c3b 100644 --- a/services/api_gateway/src/config/mod.rs +++ b/services/api_gateway/src/config/mod.rs @@ -5,7 +5,7 @@ pub mod endpoints; pub mod manager; pub mod validator; -pub use endpoints::ConfigurationServiceImpl; +pub use endpoints::GatewayConfigService; pub use manager::{ConfigItem, ConfigurationManager}; pub use validator::{ConfigValidator, ValidationRules}; diff --git a/services/api_gateway/src/grpc/monitoring_handler.rs b/services/api_gateway/src/grpc/monitoring_handler.rs index 9be443fee..b273588db 100644 --- a/services/api_gateway/src/grpc/monitoring_handler.rs +++ b/services/api_gateway/src/grpc/monitoring_handler.rs @@ -21,7 +21,7 @@ use tracing::{error, info, instrument}; use crate::monitoring::monitoring_service_server::{MonitoringService, MonitoringServiceServer}; use crate::monitoring::{ - AcknowledgeAlertRequest, AcknowledgeAlertResponse, AlertEvent, AlertEventType, + AcknowledgeAlertRequest, AcknowledgeAlertResponse, AlertEvent, ClusterPodsResponse, EpochFinancialSnapshot, GetActiveAlertsRequest, GetActiveAlertsResponse, GetEpochHistoryRequest, GetEpochHistoryResponse, GetHealthCheckRequest, GetHealthCheckResponse, GetLatencyMetricsRequest, GetLatencyMetricsResponse, @@ -625,6 +625,8 @@ impl MonitoringService for MonitoringServiceHandler { } ); + let prom = self.prom.clone(); + let stream = async_stream::stream! { let mut interval = tokio::time::interval(Duration::from_secs(u64::from(interval_secs))); loop { @@ -644,7 +646,10 @@ impl MonitoringService for MonitoringServiceHandler { }) .collect(); - let statuses = futures::future::join_all(checks).await; + let (statuses, (cpu, mem, disk)) = tokio::join!( + futures::future::join_all(checks), + prom.query_system_metrics(), + ); let healthy_count = statuses .iter() @@ -682,7 +687,12 @@ impl MonitoringService for MonitoringServiceHandler { total_services: total as i32, critical_issues, system_uptime_seconds: start_time.elapsed().as_secs() as i64, - system_metrics: Some(SystemMetrics::default()), + system_metrics: Some(SystemMetrics { + cpu_usage_percent: cpu, + memory_usage_percent: mem, + disk_usage_percent: disk, + ..Default::default() + }), }; let event = SystemStatusEvent { @@ -903,27 +913,9 @@ impl MonitoringService for MonitoringServiceHandler { request: Request, ) -> Result, Status> { let _req = request.into_inner(); - - info!("StreamAlerts: interval=10s (placeholder, no alertmanager integration yet)"); - - let stream = async_stream::stream! { - let mut interval = tokio::time::interval(Duration::from_secs(10)); - loop { - interval.tick().await; - - // Placeholder: yields an empty AlertEvent each tick. - // Will be wired to Prometheus Alertmanager when available. - let event = AlertEvent { - alert: None, - event_type: AlertEventType::Unspecified.into(), - timestamp: chrono::Utc::now().timestamp(), - }; - - yield Ok(event); - } - }; - - Ok(Response::new(Box::pin(stream))) + Err(Status::unimplemented( + "StreamAlerts will be wired to Prometheus Alertmanager", + )) } async fn acknowledge_alert( diff --git a/services/api_gateway/src/lib.rs b/services/api_gateway/src/lib.rs index 256cf0c77..7eac202f6 100644 --- a/services/api_gateway/src/lib.rs +++ b/services/api_gateway/src/lib.rs @@ -26,8 +26,6 @@ pub const ML_TRAINING_FILE_DESCRIPTOR_SET: &[u8] = tonic::include_file_descriptor_set!("ml_training_descriptor"); pub const TRADING_AGENT_FILE_DESCRIPTOR_SET: &[u8] = tonic::include_file_descriptor_set!("trading_agent_descriptor"); -pub const CONFIG_SERVICE_FILE_DESCRIPTOR_SET: &[u8] = - tonic::include_file_descriptor_set!("config_service_descriptor"); pub const TRADING_FILE_DESCRIPTOR_SET: &[u8] = tonic::include_file_descriptor_set!("trading_descriptor"); pub const RISK_FILE_DESCRIPTOR_SET: &[u8] = @@ -46,9 +44,6 @@ pub mod foxhunt { pub mod tli { tonic::include_proto!("foxhunt.tli"); } - pub mod config_proto { - tonic::include_proto!("foxhunt.config"); - } } pub mod ml_training { @@ -113,7 +108,7 @@ pub use error::{GatewayConfigError, GatewayConfigResult}; // Re-export configuration management types pub use config::{ AuthzMetrics, AuthzService, ConfigItem, ConfigValidator, ConfigurationManager, - ConfigurationServiceImpl, PermissionResult, + GatewayConfigService, PermissionResult, }; // Re-export routing and rate limiting types diff --git a/services/api_gateway/src/main.rs b/services/api_gateway/src/main.rs index 97cf86cd1..5cb5161c9 100644 --- a/services/api_gateway/src/main.rs +++ b/services/api_gateway/src/main.rs @@ -682,7 +682,6 @@ async fn main() -> Result<()> { .register_encoded_file_descriptor_set(api_gateway::MONITORING_FILE_DESCRIPTOR_SET) .register_encoded_file_descriptor_set(api_gateway::ML_TRAINING_FILE_DESCRIPTOR_SET) .register_encoded_file_descriptor_set(api_gateway::TRADING_AGENT_FILE_DESCRIPTOR_SET) - .register_encoded_file_descriptor_set(api_gateway::CONFIG_SERVICE_FILE_DESCRIPTOR_SET) .register_encoded_file_descriptor_set(api_gateway::TRADING_FILE_DESCRIPTOR_SET) .register_encoded_file_descriptor_set(api_gateway::RISK_FILE_DESCRIPTOR_SET) .register_encoded_file_descriptor_set(api_gateway::CONFIG_FILE_DESCRIPTOR_SET) diff --git a/testing/integration/fixtures/mod.rs b/testing/integration/fixtures/mod.rs index 04201fcec..74b7c5289 100644 --- a/testing/integration/fixtures/mod.rs +++ b/testing/integration/fixtures/mod.rs @@ -39,7 +39,7 @@ use tokio::sync::RwLock; use uuid::Uuid; // Import TLI types explicitly (tli crate is available as dependency) -use fxt::error::TliResult; +use fxt::error::FxtResult; // Re-export sub-modules for easy access pub mod builders; @@ -788,7 +788,7 @@ impl std::fmt::Debug for TestEnvironment { } impl TestEnvironment { - pub async fn new(config: IntegrationTestConfig) -> TliResult { + pub async fn new(config: IntegrationTestConfig) -> FxtResult { let metrics = Arc::new(TestMetricsCollector::new()); let port_manager = Arc::new(TestPortManager::new()); diff --git a/testing/integration/mocks/mod.rs b/testing/integration/mocks/mod.rs index c13ce6c2f..9e8f908a1 100644 --- a/testing/integration/mocks/mod.rs +++ b/testing/integration/mocks/mod.rs @@ -189,8 +189,8 @@ impl MockTradingService { /// # Returns /// * `Ok(MockTradingService)` - Configured mock service ready to start /// - /// * `Err(TliError)` - If port allocation or initialization failed - pub async fn new() -> TliResult { + /// * `Err(FxtError)` - If port allocation or initialization failed + pub async fn new() -> FxtResult { Self::new_with_config(MockServiceConfig::default()).await } @@ -202,8 +202,8 @@ impl MockTradingService { /// # Returns /// * `Ok(MockTradingService)` - Configured mock service /// - /// * `Err(TliError)` - If port allocation failed - pub async fn new_with_config(config: MockServiceConfig) -> TliResult { + /// * `Err(FxtError)` - If port allocation failed + pub async fn new_with_config(config: MockServiceConfig) -> FxtResult { let port = TEST_PORT_MANAGER.allocate_port().await; Ok(Self { @@ -266,8 +266,8 @@ impl MockTradingService { /// # Returns /// * `Ok(())` - Service started successfully /// - /// * `Err(TliError)` - If service failed to start - pub async fn start(&mut self) -> TliResult<()> { + /// * `Err(FxtError)` - If service failed to start + pub async fn start(&mut self) -> FxtResult<()> { let port = self.port; let config = self.config.clone(); let order_responses = Arc::clone(&self.order_responses); @@ -412,7 +412,7 @@ pub struct CreateBacktestResponse { impl MockBacktestingService { /// Create new mock backtesting service - pub async fn new() -> TliResult { + pub async fn new() -> FxtResult { let port = TEST_PORT_MANAGER.allocate_port().await; Ok(Self { @@ -498,15 +498,15 @@ impl Default for TestDatabaseConfig { impl TestDatabaseManager { /// Create new test database manager - pub async fn new() -> TliResult { + pub async fn new() -> FxtResult { Self::new_with_config(TestDatabaseConfig::default()).await } /// Create new test database manager with custom configuration - pub async fn new_with_config(config: TestDatabaseConfig) -> TliResult { + pub async fn new_with_config(config: TestDatabaseConfig) -> FxtResult { let pool = sqlx::PgPool::connect(&config.database_url) .await - .map_err(|e| TliError::DatabaseError(format!("Failed to connect to test database: {}", e)))?; + .map_err(|e| FxtError::DatabaseError(format!("Failed to connect to test database: {}", e)))?; Ok(Self { pool, config }) } @@ -517,18 +517,18 @@ impl TestDatabaseManager { } /// Verify order was persisted - pub async fn verify_order_persisted(&self, order_id: &str) -> TliResult { + pub async fn verify_order_persisted(&self, order_id: &str) -> FxtResult { let count: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM orders WHERE order_id = $1") .bind(order_id) .fetch_one(&self.pool) .await - .map_err(|e| TliError::DatabaseError(format!("Failed to verify order persistence: {}", e)))?; + .map_err(|e| FxtError::DatabaseError(format!("Failed to verify order persistence: {}", e)))?; Ok(count > 0) } /// Clean up test data - pub async fn cleanup(&self) -> TliResult<()> { + pub async fn cleanup(&self) -> FxtResult<()> { if self.config.enable_cleanup { let cleanup_queries = vec![ "DELETE FROM trading_events WHERE source LIKE '%test%'", @@ -543,7 +543,7 @@ impl TestDatabaseManager { sqlx::query(query) .execute(&self.pool) .await - .map_err(|e| TliError::DatabaseError(format!("Cleanup failed: {}", e)))?; + .map_err(|e| FxtError::DatabaseError(format!("Cleanup failed: {}", e)))?; } } @@ -558,13 +558,13 @@ pub struct TestDatabase { impl TestDatabase { /// Create new test database - pub async fn new() -> TliResult { + pub async fn new() -> FxtResult { let manager = TestDatabaseManager::new().await?; Ok(Self { manager }) } /// Verify order was persisted - pub async fn verify_order_persisted(&self, order_id: &str) -> TliResult { + pub async fn verify_order_persisted(&self, order_id: &str) -> FxtResult { self.manager.verify_order_persisted(order_id).await } } @@ -587,7 +587,7 @@ pub struct MarketDataPoint { impl TestDataProvider { /// Create new test data provider - pub async fn new() -> TliResult { + pub async fn new() -> FxtResult { let mut provider = Self { historical_data: HashMap::new(), }; @@ -648,7 +648,7 @@ pub struct MockMLModel { impl MLTestInfrastructure { /// Create new ML testing infrastructure - pub async fn new() -> TliResult { + pub async fn new() -> FxtResult { let mut models = HashMap::new(); // Add mock models with realistic characteristics