Files
foxhunt/tli/tests/e2e/train_list_e2e_test.rs
jgrusewski 7458f1be01 feat(wave12): E2E validation complete - 225-feature pipeline ready
 Validation Results:
- PPO training: 24.2s (1 epoch, 950 samples, dim=225)
- Feature extraction: 105μs/bar (9.5x faster than target)
- Model checkpoint: 293KB (147KB actor + 146KB critic)
- GPU memory: 145MB used (96.4% headroom)
- Zero dimension mismatches

📊 Success Criteria (5/5):
 Feature dimension = 225 (Wave C 201 + Wave D 24)
 Model state_dim = 225
 Training completed without errors
 Checkpoint saved successfully
 No dimension mismatch errors

📁 Training Data Ready:
- ES.FUT: 2.9MB, 180 days
- NQ.FUT: 4.4MB, 180 days
- 6E.FUT: 2.8MB, 180 days
- ZN.FUT: 65KB, 90 days (clean)

🚀 Next: Full production model retraining (4 models, ~10min GPU time)

🤖 Generated with Claude Code (https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-22 22:48:04 +02:00

215 lines
6.1 KiB
Rust

//! End-to-End Tests for `tli train list` Command
//!
//! Tests the complete flow: TLI CLI → API Gateway → ML Training Service
use tli::proto::ml_training::{
ml_training_service_server::MlTrainingServiceServer, TrainingStatus,
};
use tonic::transport::Server;
use std::net::SocketAddr;
use tokio::time::{timeout, Duration};
use super::mock_ml_training_service::{MockMlTrainingService, MockTrainingState};
use super::test_fixtures::{self, create_test_job_batch, filter_by_status, filter_by_model, create_test_job, extract_job_ids, validate_job_structure};
/// Helper to start mock gRPC server
async fn start_mock_server(state: MockTrainingState) -> SocketAddr {
let addr: SocketAddr = "127.0.0.1:0".parse().unwrap();
let service = MockMlTrainingService::new(state);
let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
let local_addr = listener.local_addr().unwrap();
tokio::spawn(async move {
Server::builder()
.add_service(MlTrainingServiceServer::new(service))
.serve_with_incoming(tokio_stream::wrappers::TcpListenerStream::new(listener))
.await
.unwrap();
});
// Give server time to start
tokio::time::sleep(Duration::from_millis(100)).await;
local_addr
}
/// TEST 1: List all jobs (no filter)
#[tokio::test]
async fn test_list_all_jobs() {
let state = MockTrainingState::default();
// Add test jobs
let jobs = create_test_job_batch();
for job in &jobs {
state.add_job(job.clone());
}
let _addr = start_mock_server(state.clone()).await;
// Verify all jobs are in state
let all_jobs = state.list_jobs();
assert_eq!(all_jobs.len(), 5, "Should have 5 jobs");
}
/// TEST 2: Filter by status (RUNNING)
#[tokio::test]
async fn test_filter_by_status_running() {
let state = MockTrainingState::default();
// Add test jobs
let jobs = create_test_job_batch();
for job in &jobs {
state.add_job(job.clone());
}
let _addr = start_mock_server(state.clone()).await;
// Filter running jobs
let all_jobs = state.list_jobs();
let running_jobs = filter_by_status(&all_jobs, TrainingStatus::Running);
assert_eq!(running_jobs.len(), 1, "Should have 1 running job");
assert_eq!(running_jobs[0].model_type, "TFT");
}
/// TEST 3: Filter by status (COMPLETED)
#[tokio::test]
async fn test_filter_by_status_completed() {
let state = MockTrainingState::default();
let jobs = create_test_job_batch();
for job in &jobs {
state.add_job(job.clone());
}
let _addr = start_mock_server(state.clone()).await;
let all_jobs = state.list_jobs();
let completed_jobs = filter_by_status(&all_jobs, TrainingStatus::Completed);
assert_eq!(completed_jobs.len(), 2, "Should have 2 completed jobs");
}
/// TEST 4: Filter by model type (TFT)
#[tokio::test]
async fn test_filter_by_model_tft() {
let state = MockTrainingState::default();
let jobs = create_test_job_batch();
for job in &jobs {
state.add_job(job.clone());
}
let _addr = start_mock_server(state.clone()).await;
let all_jobs = state.list_jobs();
let tft_jobs = filter_by_model(&all_jobs, "TFT");
assert_eq!(tft_jobs.len(), 2, "Should have 2 TFT jobs");
}
/// TEST 5: Filter by model type (DQN)
#[tokio::test]
async fn test_filter_by_model_dqn() {
let state = MockTrainingState::default();
let jobs = create_test_job_batch();
for job in &jobs {
state.add_job(job.clone());
}
let _addr = start_mock_server(state.clone()).await;
let all_jobs = state.list_jobs();
let dqn_jobs = filter_by_model(&all_jobs, "DQN");
assert_eq!(dqn_jobs.len(), 1, "Should have 1 DQN job");
assert_eq!(dqn_jobs[0].job_id, "train_dqn_nq_20251022_133000");
}
/// TEST 6: Pagination - first page
#[tokio::test]
async fn test_pagination_first_page() {
let state = MockTrainingState::default();
// Add 10 jobs
for i in 0..10 {
let job = create_test_job(
&format!("job_{}", i),
"TFT",
TrainingStatus::Completed,
);
state.add_job(job);
}
let _addr = start_mock_server(state.clone()).await;
let all_jobs = state.list_jobs();
assert_eq!(all_jobs.len(), 10);
}
/// TEST 7: Empty job list
#[tokio::test]
async fn test_empty_job_list() {
let state = MockTrainingState::default();
let _addr = start_mock_server(state.clone()).await;
let all_jobs = state.list_jobs();
assert_eq!(all_jobs.len(), 0, "Should have no jobs");
}
/// TEST 8: Combined filters (status + model)
#[tokio::test]
async fn test_combined_filters() {
let state = MockTrainingState::default();
let jobs = create_test_job_batch();
for job in &jobs {
state.add_job(job.clone());
}
let _addr = start_mock_server(state.clone()).await;
let all_jobs = state.list_jobs();
let completed_tft = all_jobs
.iter()
.filter(|j| j.status == TrainingStatus::Completed as i32 && j.model_type == "TFT")
.count();
// Only stopped TFT job in our batch
assert_eq!(completed_tft, 0, "No completed TFT jobs in test batch");
}
/// TEST 9: Job ID extraction
#[tokio::test]
async fn test_job_id_extraction() {
let state = MockTrainingState::default();
let jobs = create_test_job_batch();
for job in &jobs {
state.add_job(job.clone());
}
let _addr = start_mock_server(state.clone()).await;
let all_jobs = state.list_jobs();
let job_ids = extract_job_ids(&all_jobs);
assert_eq!(job_ids.len(), 5);
assert!(job_ids.contains(&"train_tft_es_20251022_140000".to_string()));
}
/// TEST 10: Job structure validation
#[tokio::test]
async fn test_job_structure_validation() {
let state = MockTrainingState::default();
let jobs = create_test_job_batch();
for job in &jobs {
state.add_job(job.clone());
assert!(validate_job_structure(job), "Job structure should be valid");
}
let _addr = start_mock_server(state.clone()).await;
let all_jobs = state.list_jobs();
for job in &all_jobs {
assert!(validate_job_structure(job), "All jobs should have valid structure");
}
}