using System; using System.Linq; using PolyTrader.Core.Analytics; using PolyTraderSharp.Models; using PolyTraderSharp.Services; using Xunit; namespace PolyTrader.Tests { /// Sicherheitsnetz für den reinen Dossier-Zusammenbau + das Markdown-Rendering (S-1). public class DossierBuilderTests { private static readonly DateTime T0 = new(2026, 7, 16, 10, 0, 0, DateTimeKind.Utc); private const string Sig = "sig-abc123"; private static TradeDossier SampleDossier() { var decisions = new[] { new DecisionRecord { SignalId = Sig, Timestamp = T0.AddSeconds(2), ModuleName = "CopyTrading", AccountId = 1, TokenId = "tok", MarketQuestion = "Frage?", Side = "SELL", Decision = TradeDecision.Executed, Reason = DecisionReason.LadderStarted, Message = "Leiter" }, new DecisionRecord { SignalId = Sig, Timestamp = T0, ModuleName = "CopyTrading", AccountId = 1, TokenId = "tok", MarketQuestion = "Frage?", Side = "SELL", Decision = TradeDecision.Skipped, Reason = DecisionReason.SellSpamBlock, Message = "Spam | Test" } }; var events = new[] { new OrderEventRecord { SignalId = Sig, Timestamp = T0.AddSeconds(3), EventType = OrderEventType.LadderStart, Side = "SELL", Price = 0.48m, AmountUsd = 48m, Response = "OK" } }; var trades = new[] { new TradeRecord { SignalId = Sig, ClosedAt = T0.AddMinutes(5), ModuleName = "CopyTrading", AccountId = 1, TokenId = "tok", MarketQuestion = "Frage?", EntryPrice = 0.40m, ExitPrice = 0.48m, Size = 100m, RealizedPnl = 8m, ExitReason = "Master Trader Sold" } }; var logs = new[] { new LogJson.ParsedLogLine("2026-07-16T10:00:01", "Trade", Sig, "Logzeile") }; return DossierBuilder.Build(Sig, decisions, events, trades, logs); } [Fact] public void Build_sorts_chronologically_and_derives_header() { var d = SampleDossier(); Assert.Equal(Sig, d.SignalId); Assert.Equal("CopyTrading", d.ModuleName); Assert.Equal(1, d.AccountId); Assert.Equal("tok", d.TokenId); Assert.Equal(2, d.Decisions.Count); Assert.Equal(DecisionReason.SellSpamBlock, d.Decisions[0].Reason); // früheste zuerst Assert.Single(d.OrderEvents); Assert.Single(d.ClosedTrades); Assert.Single(d.LogLines); } [Fact] public void Markdown_contains_all_sections_and_escapes_pipes() { string md = DossierBuilder.ToMarkdown(SampleDossier()); Assert.Contains("## Entscheidungen", md); Assert.Contains("## Order-Events", md); Assert.Contains("## Abgeschlossene Trades", md); Assert.Contains("## Log-Auszug", md); Assert.Contains("8,00 USDC", md.Replace("8.00", "8,00")); // PnL-Summe (kulturneutral geprüft) Assert.Contains("Spam \\| Test", md); // Pipe im Text escaped (Tabelle bleibt intakt) Assert.Contains("LadderStart", md); } [Fact] public void Markdown_for_empty_dossier_says_no_data() { var d = DossierBuilder.Build("sig-x", Enumerable.Empty(), Enumerable.Empty(), Enumerable.Empty()); Assert.Contains("Keine Daten", DossierBuilder.ToMarkdown(d)); } } }