using KArtSell.Modules.ModelOperations.FeedbackLoop; namespace KArtSell.ModelOperations.UnitTests; public sealed class ModelFeedbackCycleTests { [Fact] public void Happy_path_stops_at_human_promotion_review_and_closes() { var cycle = new ModelFeedbackCycle(Guid.NewGuid(), "KR-EQUITY", "model-1", DateTimeOffset.UtcNow); var now = DateTimeOffset.UtcNow; cycle.MoveTo(ModelFeedbackCycleState.PredictionFrozen, "FREEZE_OK", "hash-1", "system", now); cycle.MoveTo(ModelFeedbackCycleState.OutcomesMaturing, "WINDOWS_OPEN", "hash-2", "system", now); cycle.MoveTo(ModelFeedbackCycleState.Evaluated, "SCORECARD_READY", "hash-3", "system", now); cycle.MoveTo(ModelFeedbackCycleState.ImprovementProposed, "HYPOTHESIS_REVIEWED", "hash-4", "quant", now); cycle.MoveTo(ModelFeedbackCycleState.ChallengerPlanned, "PLAN_APPROVED", "hash-5", "validator", now); cycle.MoveTo(ModelFeedbackCycleState.IndependentlyValidated, "OOS_VALIDATED", "hash-6", "validator", now); cycle.MoveTo(ModelFeedbackCycleState.PromotionReviewPending, "PACK_READY", "hash-7", "risk", now); cycle.MoveTo(ModelFeedbackCycleState.Closed, "HUMAN_DECISION_RECORDED", "hash-8", "investment-committee", now); Assert.Equal(ModelFeedbackCycleState.Closed, cycle.State); Assert.Equal(9, cycle.Revision); Assert.Equal(8, cycle.Transitions.Count); } [Fact] public void Skipping_independent_validation_is_rejected() { var cycle = new ModelFeedbackCycle(Guid.NewGuid(), "US-EQUITY", "model-1", DateTimeOffset.UtcNow); Assert.Throws(() => cycle.MoveTo( ModelFeedbackCycleState.PromotionReviewPending, "SKIP", "hash", "system", DateTimeOffset.UtcNow)); } [Fact] public void Plan_contains_manual_only_activation_boundary() { var activation = Assert.Single(ModelFeedbackPlan.Stages.Where(x => x.StageCode == "ACTIVATE")); Assert.Equal("AUTOMATION_FORBIDDEN", activation.AutomationBoundary); Assert.Contains("NO_AUTOMATIC_MODEL_ACTIVATION", ModelFeedbackPlan.Boundary); } }