namespace KArtSell.Integration.Tests.SellDecision; using KArtSell.Modules.ModelOperations.SellDecision; using Xunit; public class PboValidatorTests { private readonly IPboValidator _validator = new PboValidator(); [Fact] public void ValidatePboScore_ValidScore_ReturnsTrue() { var (isValid, reason) = _validator.ValidatePboScore(0.72m, 0.65m); Assert.True(isValid); Assert.Contains(">=", reason); } [Fact] public void ValidatePboScore_InvalidScore_ReturnsFalse() { var (isValid, reason) = _validator.ValidatePboScore(0.55m, 0.65m); Assert.False(isValid); Assert.Contains("Backtest overfit risk", reason); } [Fact] public void ValidatePboScore_NullScore_ReturnsFalse() { var (isValid, reason) = _validator.ValidatePboScore(null, 0.65m); Assert.False(isValid); Assert.Contains("not yet available", reason); } } public class DsrValidatorTests { private readonly IDsrValidator _validator = new DsrValidator(); [Fact] public void ValidateDsrMetric_ValidMetric_ReturnsTrue() { var (isValid, reason) = _validator.ValidateDsrMetric(0.018m, 0.015m); Assert.True(isValid); Assert.Contains(">=", reason); } [Fact] public void ValidateDsrMetric_InvalidMetric_ReturnsFalse() { var (isValid, reason) = _validator.ValidateDsrMetric(0.010m, 0.015m); Assert.False(isValid); Assert.Contains("Daily Sharpe ratio", reason); } [Fact] public void ValidateDsrMetric_NullMetric_ReturnsFalse() { var (isValid, reason) = _validator.ValidateDsrMetric(null, 0.015m); Assert.False(isValid); Assert.Contains("not yet available", reason); } } public class OosValidatorTests { private readonly IOosValidator _validator = new OosValidator(); [Fact] public void ValidateOosPerformance_ValidReturn_ReturnsTrue() { var (isValid, reason) = _validator.ValidateOosPerformance("0.08", 0.05m); Assert.True(isValid); Assert.Contains(">=", reason); } [Fact] public void ValidateOosPerformance_InvalidReturn_ReturnsFalse() { var (isValid, reason) = _validator.ValidateOosPerformance("0.03", 0.05m); Assert.False(isValid); Assert.Contains("underperforms", reason); } [Fact] public void ValidateOosPerformance_NullData_ReturnsFalse() { var (isValid, reason) = _validator.ValidateOosPerformance(null, 0.05m); Assert.False(isValid); Assert.Contains("not yet available", reason); } } public class SellPriorityRankerTests { private readonly ISellPriorityRanker _ranker = new SellPriorityRanker(); [Fact] public void RankByPolicy_HardImpairment_ReturnsHardImpairment() { var priority = _ranker.RankByPolicy(drawdown: -0.35m, marginRatio: 0.5m, concentration: 0.1m, liquidity: 0.8m, daysHeld: 100); Assert.Equal(SellPriority.HardImpairment, priority); } [Fact] public void RankByPolicy_PortfolioSurvival_ReturnsPortfolioSurvival() { var priority = _ranker.RankByPolicy(drawdown: 0m, marginRatio: 0.15m, concentration: 0.1m, liquidity: 0.8m, daysHeld: 100); Assert.Equal(SellPriority.PortfolioSurvival, priority); } [Fact] public void RankByPolicy_Concentration_ReturnsConcentration() { var priority = _ranker.RankByPolicy(drawdown: -0.05m, marginRatio: 0.5m, concentration: 0.3m, liquidity: 0.8m, daysHeld: 100); Assert.Equal(SellPriority.Concentration, priority); } [Fact] public void CalculateScore_HardImpairment_ReturnsLowestScore() { var score = _ranker.CalculateScore(SellPriority.HardImpairment, fundAgeDays: 400, liquidityPercent: 0.5m); Assert.Equal(950m, score); // 1000 - 50 (age boost, fundAgeDays > 365 per VS-10-SLICE_SPEC.md) } [Fact] public void CalculateScore_ReentryOption_ReturnsHighestScore() { var score = _ranker.CalculateScore(SellPriority.ReentryOption, fundAgeDays: 100, liquidityPercent: 0.5m); Assert.Equal(50m, score); // No boosts applied } [Fact] public void CalculateScore_IlliquidFund_ReducesScore() { var scoreHighLiquidity = _ranker.CalculateScore(SellPriority.Concentration, fundAgeDays: 100, liquidityPercent: 0.5m); var scoreLowLiquidity = _ranker.CalculateScore(SellPriority.Concentration, fundAgeDays: 100, liquidityPercent: 0.1m); Assert.True(scoreLowLiquidity < scoreHighLiquidity); // Illiquid = higher priority (lower score) } } public class SellDecisionEntityTests { [Fact] public void SellDecisionEntity_CreatedWithAllFields_StoresCorrectly() { var now = DateTime.UtcNow; var entity = new SellDecisionEntity { Id = Guid.NewGuid(), ModelId = Guid.NewGuid(), Status = "PENDING", PboScore = 0.72m, DsrMetric = 0.018m, OosPerformance = "0.08", SellPriority = 1, TargetQuantity = 500, TargetPrice = 150.25m, CreatedAt = now, CreatedBy = "user@example.com", CreatedJustification = "test", PublishedAt = now, CorrelationId = Guid.NewGuid(), Revision = 1 }; Assert.Equal("PENDING", entity.Status); Assert.Equal(0.72m, entity.PboScore); } } public class SellDecisionStateTransitionTests { [Theory] [InlineData("PENDING", "SIGNAL_GENERATED", true)] [InlineData("SIGNAL_GENERATED", "PBO_VALIDATED", true)] [InlineData("PBO_VALIDATED", "DSR_VALIDATED", true)] [InlineData("DSR_VALIDATED", "OOS_APPROVED", true)] [InlineData("OOS_APPROVED", "READY_FOR_APPROVAL", true)] [InlineData("READY_FOR_APPROVAL", "APPROVED", true)] [InlineData("APPROVED", "EXECUTED", true)] [InlineData("EXECUTED", "CONFIRMED", true)] [InlineData("PENDING", "APPROVED", false)] // Invalid: skipping states public void StateTransition_ValidatesAllowedPaths(string fromState, string toState, bool shouldBeValid) { var validTransitions = new[] { ("PENDING", "SIGNAL_GENERATED"), ("SIGNAL_GENERATED", "PBO_VALIDATED"), ("PBO_VALIDATED", "DSR_VALIDATED"), ("DSR_VALIDATED", "OOS_APPROVED"), ("OOS_APPROVED", "READY_FOR_APPROVAL"), ("READY_FOR_APPROVAL", "APPROVED"), ("APPROVED", "EXECUTED"), ("EXECUTED", "CONFIRMED") }; var isValid = validTransitions.Contains((fromState, toState)); Assert.Equal(shouldBeValid, isValid); } } public class SellDecisionPitTrackingTests { [Fact] public void SellDecision_IncludesCorrelationIdForTracing() { var correlationId = Guid.NewGuid(); var entity = new SellDecisionEntity { Id = Guid.NewGuid(), ModelId = Guid.NewGuid(), Status = "PENDING", OosPerformance = "0.08", CreatedBy = "user@example.com", CreatedJustification = "test", CorrelationId = correlationId, PublishedAt = DateTime.UtcNow, Revision = 1 }; Assert.Equal(correlationId, entity.CorrelationId); } [Fact] public void SellDecision_TracksRevisionOnUpdate() { var entity = new SellDecisionEntity { Status = "PENDING", OosPerformance = "0.08", CreatedBy = "user@example.com", CreatedJustification = "test", Revision = 1 }; entity.Revision++; // Simulate update Assert.Equal(2, entity.Revision); } [Fact] public void SellDecision_HasPublishedAtTimestamp() { var now = DateTime.UtcNow; var entity = new SellDecisionEntity { Id = Guid.NewGuid(), Status = "PENDING", OosPerformance = "0.08", CreatedBy = "user@example.com", CreatedJustification = "test", PublishedAt = now }; Assert.Equal(now, entity.PublishedAt); } } public class SellDecisionIdempotencyTests { [Fact] public void SellDecision_WithSameCorrelationId_ShouldBeTreatedAsIdempotent() { var correlationId = Guid.NewGuid(); var decision1 = new SellDecisionEntity { Id = Guid.NewGuid(), Status = "PENDING", OosPerformance = "0.08", CreatedBy = "user@example.com", CreatedJustification = "test", CorrelationId = correlationId, Revision = 1 }; var decision2 = new SellDecisionEntity { Id = Guid.NewGuid(), Status = "PENDING", OosPerformance = "0.08", CreatedBy = "user@example.com", CreatedJustification = "test", CorrelationId = correlationId, Revision = 1 }; // Both have same correlation ID, so duplicate creation should be rejected Assert.Equal(decision1.CorrelationId, decision2.CorrelationId); } } public class SellDecisionContractIntegrationTests { [Fact] public void CreateSellDecisionRequest_ValidatesAllRequiredFields() { var request = new CreateSellDecisionRequest { ModelId = Guid.NewGuid(), WindowStart = DateTime.UtcNow.AddDays(-90), WindowEnd = DateTime.UtcNow, ThresholdPbo = 0.65m, ThresholdDsr = 0.015m, Justification = "Model consensus" }; Assert.NotEqual(Guid.Empty, request.ModelId); Assert.NotEmpty(request.Justification); } [Fact] public void CreateSellDecisionResponse_ContainsRequiredFields() { var response = new CreateSellDecisionResponse { DecisionId = Guid.NewGuid(), ModelId = Guid.NewGuid(), Status = "PENDING", CorrelationId = Guid.NewGuid(), CreatedAt = DateTime.UtcNow }; Assert.NotEqual(Guid.Empty, response.DecisionId); Assert.NotEmpty(response.Status); } [Fact] public void ExecuteSellDecisionRequest_ValidatesApprovalLinkage() { var request = new ExecuteSellDecisionRequest { ApprovalId = Guid.NewGuid(), ExecutionPrice = 150.25m, Quantity = 500, Justification = "Approved via VS-03" }; Assert.NotEqual(Guid.Empty, request.ApprovalId); Assert.True(request.ExecutionPrice > 0); Assert.True(request.Quantity > 0); } }