using QuantEngine.Application.Services; using QuantEngine.Core.Interfaces; namespace QuantEngine.Core.Tests; public class HistoryIngestionE2ETests { [Fact] public async Task AppendDecisionThenReadSnapshotRoundTripsThroughApplicationFlow() { var store = new FakeHistoryStore(); var ingestion = new HistoryIngestionService(store); var reader = new PostgresqlHistorySnapshotReader(store); var appendCount = await ingestion.AppendDecisionAsync(new Dictionary { ["decision_id"] = "dec-001", ["decided_at"] = DateTimeOffset.Parse("2026-06-26T09:00:00+09:00"), ["instrument_id"] = "005930", ["action"] = "BUY", ["gate"] = "PASS", ["score"] = 87.5, ["source_version"] = "v1", ["provenance"] = new Dictionary { ["source"] = "unit-test" } }); Assert.Equal(1, appendCount); var rows = await reader.ReadAsync("decision_result_history", 10); Assert.Single(rows); Assert.Equal("dec-001", rows[0]["decision_id"]); Assert.Equal("005930", rows[0]["instrument_id"]); Assert.Equal("BUY", rows[0]["action"]); Assert.Equal("PASS", rows[0]["gate"]); Assert.Equal(87.5, rows[0]["score"]); } [Fact] public async Task AppendFactorOutputThenReadSnapshotPreservesPayload() { var store = new FakeHistoryStore(); var ingestion = new HistoryIngestionService(store); var reader = new PostgresqlHistorySnapshotReader(store); var appendCount = await ingestion.AppendFactorOutputAsync( factorId: "RS_VERDICT_V2", factorVersion: "2026-06-26", outputValue: 1.23, outputGate: "PASS", sourceVersion: "source-42", observedAt: DateTimeOffset.Parse("2026-06-26T10:00:00+09:00")); Assert.Equal(1, appendCount); var rows = await reader.ReadAsync("factor_output_history", 10); Assert.Single(rows); Assert.Equal("RS_VERDICT_V2", rows[0]["factor_id"]); Assert.Equal("2026-06-26", rows[0]["factor_version"]); Assert.Equal(1.23, rows[0]["output_value"]); Assert.Equal("PASS", rows[0]["output_gate"]); Assert.Equal("source-42", rows[0]["source_version"]); } private sealed class FakeHistoryStore : IPostgresqlHistoryStore { private readonly Dictionary>> _rows = new(); public Task AppendAsync(string domain, IDictionary payload) { if (!_rows.TryGetValue(domain, out var list)) { list = new List>(); _rows[domain] = list; } list.Add(new Dictionary(payload)); return Task.FromResult(1); } public Task>> SnapshotAsync(string domain, int limit = 500) { if (!_rows.TryGetValue(domain, out var list)) { return Task.FromResult>>(Array.Empty>()); } return Task.FromResult>>(list.Take(limit).ToList()); } } }