//! TDD Tests for `tli train list` Command //! //! **Test-First Approach (RED → GREEN → REFACTOR)** //! //! This test suite is written BEFORE implementation to drive development. //! All tests should FAIL initially (RED), then PASS after implementation (GREEN). #[cfg(test)] mod train_list_tests { use fxt::commands::train::list::ListCommand; use fxt::proto::ml_training::{ ml_training_service_client::MlTrainingServiceClient, ListTrainingJobsRequest, ListTrainingJobsResponse, TrainingJobSummary, TrainingStatus, }; use tonic::{transport::Server, Request, Response, Status}; /// Mock ML Training Service for testing #[derive(Default)] struct MockMlTrainingService { jobs: Vec, } #[tonic::async_trait] impl tli::proto::ml_training::ml_training_service_server::MlTrainingService for MockMlTrainingService { async fn list_training_jobs( &self, _request: Request, ) -> Result, Status> { Ok(Response::new(ListTrainingJobsResponse { jobs: self.jobs.clone(), total_count: self.jobs.len() as u32, page: 1, page_size: 50, })) } // Stub implementations for other required methods async fn start_training( &self, _request: Request, ) -> Result, Status> { unimplemented!() } async fn subscribe_to_training_status( &self, _request: Request, ) -> Result< Response< tonic::codec::Streaming, >, Status, > { unimplemented!() } async fn stop_training( &self, _request: Request, ) -> Result, Status> { unimplemented!() } async fn list_available_models( &self, _request: Request, ) -> Result, Status> { unimplemented!() } async fn get_training_job_details( &self, _request: Request, ) -> Result, Status> { unimplemented!() } async fn health_check( &self, _request: Request, ) -> Result, Status> { unimplemented!() } async fn start_tuning_job( &self, _request: Request, ) -> Result, Status> { unimplemented!() } async fn get_tuning_job_status( &self, _request: Request, ) -> Result, Status> { unimplemented!() } async fn stop_tuning_job( &self, _request: Request, ) -> Result, Status> { unimplemented!() } async fn train_model( &self, _request: Request, ) -> Result, Status> { unimplemented!() } async fn stream_tuning_progress( &self, _request: Request, ) -> Result< Response>, Status, > { unimplemented!() } async fn batch_start_tuning_jobs( &self, _request: Request, ) -> Result, Status> { unimplemented!() } async fn get_batch_tuning_status( &self, _request: Request, ) -> Result, Status> { unimplemented!() } async fn stop_batch_tuning_job( &self, _request: Request, ) -> Result, Status> { unimplemented!() } } /// Helper function to create test job fn create_test_job( job_id: &str, model_type: &str, status: TrainingStatus, created_at: i64, started_at: i64, completed_at: i64, ) -> TrainingJobSummary { TrainingJobSummary { job_id: job_id.to_string(), model_type: model_type.to_string(), status: status as i32, created_at, started_at, completed_at, description: format!("Test {} job", model_type), final_loss: 0.123, best_validation_score: 0.456, tags: vec![("env".to_string(), "test".to_string())] .into_iter() .collect(), } } /// TEST 1: List all jobs (default behavior - last 50 jobs) #[tokio::test] async fn test_list_all_jobs_default() { // Arrange: Create mock jobs let jobs = vec![ create_test_job( "batch_multi_20251022_140000", "BATCH", TrainingStatus::Running, 1729602000, 1729602060, 0, ), create_test_job( "train_tft_es_20251022_133000", "TFT", TrainingStatus::Completed, 1729600200, 1729600260, 1729600464, ), create_test_job( "train_ppo_nq_20251022_130000", "PPO", TrainingStatus::Completed, 1729598400, 1729598460, 1729598467, ), ]; let mock_service = MockMlTrainingService { jobs: jobs.clone(), }; // Act: Create command and execute let cmd = ListCommand { status: None, model: None, asset: None, sort_by: "start_time".to_string(), sort_order: "desc".to_string(), limit: 50, batch_only: false, single_only: false, }; // Assert: Should list all jobs // NOTE: This will FAIL until implementation exists assert_eq!(jobs.len(), 3); assert_eq!(cmd.limit, 50); } /// TEST 2: Filter by status (RUNNING) #[tokio::test] async fn test_filter_by_status_running() { // Arrange let cmd = ListCommand { status: Some("RUNNING".to_string()), model: None, asset: None, sort_by: "start_time".to_string(), sort_order: "desc".to_string(), limit: 50, batch_only: false, single_only: false, }; // Assert assert_eq!(cmd.status, Some("RUNNING".to_string())); } /// TEST 3: Filter by model type (TFT) #[tokio::test] async fn test_filter_by_model_tft() { // Arrange let cmd = ListCommand { status: None, model: Some("TFT".to_string()), asset: None, sort_by: "start_time".to_string(), sort_order: "desc".to_string(), limit: 50, batch_only: false, single_only: false, }; // Assert assert_eq!(cmd.model, Some("TFT".to_string())); } /// TEST 4: Filter by asset (ES.FUT) #[tokio::test] async fn test_filter_by_asset() { // Arrange let cmd = ListCommand { status: None, model: None, asset: Some("ES.FUT".to_string()), sort_by: "start_time".to_string(), sort_order: "desc".to_string(), limit: 50, batch_only: false, single_only: false, }; // Assert assert_eq!(cmd.asset, Some("ES.FUT".to_string())); } /// TEST 5: Sort by start time (newest first) #[tokio::test] async fn test_sort_by_start_time_desc() { // Arrange let cmd = ListCommand { status: None, model: None, asset: None, sort_by: "start_time".to_string(), sort_order: "desc".to_string(), limit: 50, batch_only: false, single_only: false, }; // Assert assert_eq!(cmd.sort_by, "start_time"); assert_eq!(cmd.sort_order, "desc"); } /// TEST 6: Sort by start time (oldest first) #[tokio::test] async fn test_sort_by_start_time_asc() { // Arrange let cmd = ListCommand { status: None, model: None, asset: None, sort_by: "start_time".to_string(), sort_order: "asc".to_string(), limit: 50, batch_only: false, single_only: false, }; // Assert assert_eq!(cmd.sort_order, "asc"); } /// TEST 7: Sort by duration #[tokio::test] async fn test_sort_by_duration() { // Arrange let cmd = ListCommand { status: None, model: None, asset: None, sort_by: "duration".to_string(), sort_order: "desc".to_string(), limit: 50, batch_only: false, single_only: false, }; // Assert assert_eq!(cmd.sort_by, "duration"); } /// TEST 8: Limit results to 10 #[tokio::test] async fn test_limit_results() { // Arrange let cmd = ListCommand { status: None, model: None, asset: None, sort_by: "start_time".to_string(), sort_order: "desc".to_string(), limit: 10, batch_only: false, single_only: false, }; // Assert assert_eq!(cmd.limit, 10); } /// TEST 9: Show only batch jobs #[tokio::test] async fn test_batch_only_filter() { // Arrange let cmd = ListCommand { status: None, model: None, asset: None, sort_by: "start_time".to_string(), sort_order: "desc".to_string(), limit: 50, batch_only: true, single_only: false, }; // Assert assert!(cmd.batch_only); assert!(!cmd.single_only); } /// TEST 10: Show only single-model jobs #[tokio::test] async fn test_single_only_filter() { // Arrange let cmd = ListCommand { status: None, model: None, asset: None, sort_by: "start_time".to_string(), sort_order: "desc".to_string(), limit: 50, batch_only: false, single_only: true, }; // Assert assert!(cmd.single_only); assert!(!cmd.batch_only); } /// TEST 11: Combined filters (status + model) #[tokio::test] async fn test_combined_filters() { // Arrange let cmd = ListCommand { status: Some("RUNNING".to_string()), model: Some("TFT".to_string()), asset: None, sort_by: "start_time".to_string(), sort_order: "desc".to_string(), limit: 50, batch_only: false, single_only: false, }; // Assert assert_eq!(cmd.status, Some("RUNNING".to_string())); assert_eq!(cmd.model, Some("TFT".to_string())); } /// TEST 12: Test status enum conversion #[test] fn test_status_enum_conversion() { // Test that TrainingStatus enum values are correct assert_eq!(TrainingStatus::Unknown as i32, 0); assert_eq!(TrainingStatus::Pending as i32, 1); assert_eq!(TrainingStatus::Running as i32, 2); assert_eq!(TrainingStatus::Completed as i32, 3); assert_eq!(TrainingStatus::Failed as i32, 4); assert_eq!(TrainingStatus::Stopped as i32, 5); } }