using Moq; using QuantEngine.Application.Services; using QuantEngine.Core.Interfaces; namespace QuantEngine.Core.Tests; public class DecisionLearningServiceTests { [Fact] public async Task RecordDecisionAsync_TrimsAndPersistsNormalizedPayloads() { var store = new Mock(MockBehavior.Strict); Guid decisionId = Guid.NewGuid(); Guid factorObservationId = Guid.NewGuid(); Guid sourceObservationId = Guid.NewGuid(); store.Setup(s => s.AppendDecisionAsync(It.Is(r => r.DecisionKey == "decision-1" && r.InstrumentId == "005930" && r.Action == "BUY" && r.Gate == "PASS" && r.SourceVersion == "v1" && r.TraceJson.Contains("\"mode\":\"test\"") && r.ProvenanceJson.Contains("\"origin\":\"unit\"")))).ReturnsAsync(decisionId); store.Setup(s => s.AppendSourceObservationAsync(It.Is(r => r.InstrumentId == "005930" && r.SourceName == "kis" && r.SourceVersion == "v1" && r.PayloadJson == "{}" && r.ProvenanceJson == "{}"))).ReturnsAsync(sourceObservationId); store.Setup(s => s.AppendFactorObservationAsync(It.Is(r => r.ObservationId == sourceObservationId && r.FactorObservationId != Guid.Empty && r.FactorId == "factor_a" && r.FactorVersion == "1.0" && r.Gate == "PASS" && r.ProvenanceJson == "{}"))).ReturnsAsync(factorObservationId); store.Setup(s => s.AppendDecisionFactorEvidenceAsync(decisionId, factorObservationId, "primary")) .Returns(Task.CompletedTask); var service = new DecisionLearningService(store.Object); var result = await service.RecordDecisionAsync( " decision-1 ", DateTimeOffset.Parse("2026-07-13T00:00:00Z"), " 005930 ", " BUY ", " PASS ", 12.5m, " v1 ", new[] { new FactorEvidenceInput( Guid.NewGuid(), " factor_a ", " 1.0 ", DateTimeOffset.Parse("2026-07-13T00:00:00Z"), 3.14m, null, " PASS ", " primary ", " kis ", "{}", "{}") }, new { mode = "test" }, new { origin = "unit" }); Assert.Equal(decisionId, result); store.VerifyAll(); } [Fact] public async Task RecordDecisionAsync_RejectsMissingFactors() { var service = new DecisionLearningService(new Mock().Object); await Assert.ThrowsAsync(() => service.RecordDecisionAsync( "decision-1", DateTimeOffset.UtcNow, "005930", "BUY", "PASS", null, "v1", null!)); } }