Files
foxhunt/bin/fxt/tests/commands/train_list_test.rs
jgrusewski 9c3d741a08 refactor: restructure repo — crates/, bin/, testing/ layout
Move 17 library crates into crates/, CLI binary into bin/fxt,
consolidate 10 test crates into testing/, split config crate
from deployment config files.

Root directory reduced from 38+ to ~17 directories.
All Cargo.toml paths and build.rs proto refs updated.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 11:56:00 +01:00

437 lines
13 KiB
Rust

//! 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<TrainingJobSummary>,
}
#[tonic::async_trait]
impl tli::proto::ml_training::ml_training_service_server::MlTrainingService
for MockMlTrainingService
{
async fn list_training_jobs(
&self,
_request: Request<ListTrainingJobsRequest>,
) -> Result<Response<ListTrainingJobsResponse>, 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<tli::proto::ml_training::StartTrainingRequest>,
) -> Result<Response<tli::proto::ml_training::StartTrainingResponse>, Status> {
unimplemented!()
}
async fn subscribe_to_training_status(
&self,
_request: Request<tli::proto::ml_training::SubscribeToTrainingStatusRequest>,
) -> Result<
Response<
tonic::codec::Streaming<tli::proto::ml_training::TrainingStatusUpdate>,
>,
Status,
> {
unimplemented!()
}
async fn stop_training(
&self,
_request: Request<tli::proto::ml_training::StopTrainingRequest>,
) -> Result<Response<tli::proto::ml_training::StopTrainingResponse>, Status> {
unimplemented!()
}
async fn list_available_models(
&self,
_request: Request<tli::proto::ml_training::ListAvailableModelsRequest>,
) -> Result<Response<tli::proto::ml_training::ListAvailableModelsResponse>, Status>
{
unimplemented!()
}
async fn get_training_job_details(
&self,
_request: Request<tli::proto::ml_training::GetTrainingJobDetailsRequest>,
) -> Result<Response<tli::proto::ml_training::GetTrainingJobDetailsResponse>, Status>
{
unimplemented!()
}
async fn health_check(
&self,
_request: Request<tli::proto::ml_training::HealthCheckRequest>,
) -> Result<Response<tli::proto::ml_training::HealthCheckResponse>, Status> {
unimplemented!()
}
async fn start_tuning_job(
&self,
_request: Request<tli::proto::ml_training::StartTuningJobRequest>,
) -> Result<Response<tli::proto::ml_training::StartTuningJobResponse>, Status> {
unimplemented!()
}
async fn get_tuning_job_status(
&self,
_request: Request<tli::proto::ml_training::GetTuningJobStatusRequest>,
) -> Result<Response<tli::proto::ml_training::GetTuningJobStatusResponse>, Status>
{
unimplemented!()
}
async fn stop_tuning_job(
&self,
_request: Request<tli::proto::ml_training::StopTuningJobRequest>,
) -> Result<Response<tli::proto::ml_training::StopTuningJobResponse>, Status> {
unimplemented!()
}
async fn train_model(
&self,
_request: Request<tli::proto::ml_training::TrainModelRequest>,
) -> Result<Response<tli::proto::ml_training::TrainModelResponse>, Status> {
unimplemented!()
}
async fn stream_tuning_progress(
&self,
_request: Request<tli::proto::ml_training::StreamProgressRequest>,
) -> Result<
Response<tonic::codec::Streaming<tli::proto::ml_training::ProgressUpdate>>,
Status,
> {
unimplemented!()
}
async fn batch_start_tuning_jobs(
&self,
_request: Request<tli::proto::ml_training::BatchStartTuningJobsRequest>,
) -> Result<Response<tli::proto::ml_training::BatchStartTuningJobsResponse>, Status>
{
unimplemented!()
}
async fn get_batch_tuning_status(
&self,
_request: Request<tli::proto::ml_training::GetBatchTuningStatusRequest>,
) -> Result<Response<tli::proto::ml_training::GetBatchTuningStatusResponse>, Status>
{
unimplemented!()
}
async fn stop_batch_tuning_job(
&self,
_request: Request<tli::proto::ml_training::StopBatchTuningJobRequest>,
) -> Result<Response<tli::proto::ml_training::StopBatchTuningJobResponse>, 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);
}
}