feat(serving): wire list_pending_promotions to PromotionManager
Replace the stub list_pending_promotions gRPC handler with a real implementation that queries PromotionManager::list_pending() and maps each PendingModel to the proto PendingPromotion message. Adds a unit test verifying the field mapping from domain type to proto type. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -35,7 +35,7 @@ use proto::{
|
||||
TrainingStatusUpdate as ProtoStatusUpdate,
|
||||
// Job completion & model promotion (on-demand training pipeline)
|
||||
JobCompletionReport, JobCompletionAck,
|
||||
ListPendingPromotionsRequest, ListPendingPromotionsResponse,
|
||||
ListPendingPromotionsRequest, ListPendingPromotionsResponse, PendingPromotion,
|
||||
ApprovePromotionRequest, ApprovePromotionResponse,
|
||||
RejectPromotionRequest, RejectPromotionResponse,
|
||||
};
|
||||
@@ -1234,10 +1234,24 @@ impl MlTrainingService for MLTrainingServiceImpl {
|
||||
&self,
|
||||
_request: Request<ListPendingPromotionsRequest>,
|
||||
) -> Result<Response<ListPendingPromotionsResponse>, Status> {
|
||||
// TODO(task-6): query promotion store
|
||||
Ok(Response::new(ListPendingPromotionsResponse {
|
||||
promotions: vec![],
|
||||
}))
|
||||
let pending = self.promotion_manager.list_pending().await;
|
||||
info!(count = pending.len(), "listing pending promotions");
|
||||
|
||||
let promotions = pending
|
||||
.into_iter()
|
||||
.map(|m| PendingPromotion {
|
||||
model_id: m.model_id,
|
||||
model_type: m.model_type,
|
||||
symbol: m.symbol,
|
||||
s3_path: m.s3_path,
|
||||
new_metrics: m.new_metrics.into_iter().collect(),
|
||||
current_metrics: m.current_metrics.into_iter().collect(),
|
||||
trained_at: m.trained_at.timestamp(),
|
||||
job_id: m.job_id,
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(Response::new(ListPendingPromotionsResponse { promotions }))
|
||||
}
|
||||
|
||||
async fn approve_promotion(
|
||||
@@ -1757,4 +1771,50 @@ mod tests {
|
||||
crate::promotion_manager::PromotionStatus::Registered
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list_pending_promotions_maps_to_proto() {
|
||||
// Verify that PendingModel fields map correctly to proto PendingPromotion
|
||||
use crate::promotion_manager::PendingModel;
|
||||
use chrono::Utc;
|
||||
|
||||
let now = Utc::now();
|
||||
let model = PendingModel {
|
||||
model_id: "m-123".to_string(),
|
||||
model_type: "DQN".to_string(),
|
||||
symbol: "ES.FUT".to_string(),
|
||||
s3_path: "s3://bucket/better.bin".to_string(),
|
||||
new_metrics: HashMap::from([
|
||||
("best_val_loss".to_string(), 0.03),
|
||||
("sharpe_ratio".to_string(), 2.5),
|
||||
]),
|
||||
current_metrics: HashMap::from([
|
||||
("best_val_loss".to_string(), 0.10),
|
||||
("sharpe_ratio".to_string(), 1.0),
|
||||
]),
|
||||
trained_at: now,
|
||||
job_id: "job-99".to_string(),
|
||||
};
|
||||
|
||||
// Map to proto the same way the handler does
|
||||
let proto = super::proto::PendingPromotion {
|
||||
model_id: model.model_id.clone(),
|
||||
model_type: model.model_type.clone(),
|
||||
symbol: model.symbol.clone(),
|
||||
s3_path: model.s3_path.clone(),
|
||||
new_metrics: model.new_metrics.clone().into_iter().collect(),
|
||||
current_metrics: model.current_metrics.clone().into_iter().collect(),
|
||||
trained_at: model.trained_at.timestamp(),
|
||||
job_id: model.job_id.clone(),
|
||||
};
|
||||
|
||||
assert_eq!(proto.model_id, "m-123");
|
||||
assert_eq!(proto.model_type, "DQN");
|
||||
assert_eq!(proto.symbol, "ES.FUT");
|
||||
assert_eq!(proto.s3_path, "s3://bucket/better.bin");
|
||||
assert_eq!(proto.job_id, "job-99");
|
||||
assert_eq!(proto.trained_at, now.timestamp());
|
||||
assert_eq!(proto.new_metrics.len(), 2);
|
||||
assert_eq!(proto.current_metrics.len(), 2);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user