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>
437 lines
13 KiB
Rust
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);
|
|
}
|
|
}
|