Fix: appsettings.Local.json im Host laden + headless UI-Smoke-Test

- KRITISCH: Host.CreateDefaultBuilder lädt appsettings.Local.json nicht (nur
  appsettings.json + appsettings.{Environment}.json). Dadurch war
  Database:MySqlConnectionString zur Laufzeit leer -> Pomelo fiel auf
  localhost:3306 zurück ("Connect Timeout expired"), die GUI-App hätte am
  Server nie mit MySQL verbunden. Fix: appsettings.Local.json explizit per
  ConfigureAppConfiguration ergänzen (in Main und im Smoke-Test).
- Neuer CLI-Modus --smoke-ui: baut einen minimalen Host, hydriert den State
  aus MySQL und konstruiert jede registrierte View + den Launcher (ohne
  Message-Loop / Trading-Services). Deckte den obigen Bug auf.
- Verifiziert: 3 Accounts / 32 Trader hydriert; alle 3 Modul-Views + Launcher
  konstruieren fehlerfrei mit echten Daten.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Richard
2026-07-06 09:42:56 +02:00
co-authored by Claude Opus 4.8
parent 05e0576965
commit 6f478e8764
+98 -1
View File
@@ -40,6 +40,14 @@ internal static class Program
return; return;
} }
// Headless-Smoke-Test der UI: konstruiert jede View + den Launcher (ohne Message-Loop
// und ohne Trading-/WSS-Services). Fängt Laufzeit-Konstruktionsfehler ab. Kein Fenster.
if (args.Length > 0 && string.Equals(args[0], "--smoke-ui", StringComparison.OrdinalIgnoreCase))
{
Environment.ExitCode = RunSmokeUi();
return;
}
ApplicationConfiguration.Initialize(); ApplicationConfiguration.Initialize();
var modules = new System.Collections.Generic.List<IPolyTraderModule> var modules = new System.Collections.Generic.List<IPolyTraderModule>
@@ -47,7 +55,13 @@ internal static class Program
new CopyTradingModule() new CopyTradingModule()
}; };
AppHost = Host.CreateDefaultBuilder().ConfigureServices(delegate(HostBuilderContext context, IServiceCollection services) AppHost = Host.CreateDefaultBuilder()
// Host.CreateDefaultBuilder lädt appsettings.Local.json NICHT (nur appsettings.json
// + appsettings.{Environment}.json). Die gitignorierte Local-Datei hält aber die
// MySQL-Connection daher hier explizit ergänzen, sonst bleibt sie leer.
.ConfigureAppConfiguration((context, config) =>
config.AddJsonFile("appsettings.Local.json", optional: true, reloadOnChange: false))
.ConfigureServices(delegate(HostBuilderContext context, IServiceCollection services)
{ {
var databaseOptions = new DatabaseOptions var databaseOptions = new DatabaseOptions
{ {
@@ -237,4 +251,87 @@ internal static class Program
Services.ConfigMigrator.VerifyMySql(mySql); Services.ConfigMigrator.VerifyMySql(mySql);
} }
/// <summary>
/// Headless-Smoke-Test: baut einen minimalen Host (Persistenz + State + Modul-Registrierung
/// + Shell/Launcher), hydriert den State aus MySQL und konstruiert jede registrierte View
/// sowie den Launcher ohne Message-Loop und ohne Trading-/WSS-Services zu starten.
/// Gibt 0 zurück, wenn alles fehlerfrei konstruiert, sonst die Fehleranzahl.
/// </summary>
private static int RunSmokeUi()
{
ApplicationConfiguration.Initialize();
var modules = new System.Collections.Generic.List<IPolyTraderModule> { new CopyTradingModule() };
using var host = Host.CreateDefaultBuilder()
.ConfigureAppConfiguration((context, config) =>
config.AddJsonFile("appsettings.Local.json", optional: true, reloadOnChange: false))
.ConfigureServices((context, services) =>
{
var databaseOptions = new DatabaseOptions
{
MySqlConnectionString = context.Configuration["Database:MySqlConnectionString"] ?? string.Empty
};
services.Configure<DatabaseOptions>(context.Configuration.GetSection(DatabaseOptions.SectionName));
services.AddCorePersistence(databaseOptions);
services.AddSingleton<TerminalLogger>();
services.AddSingleton<TradingState>();
services.AddSingleton<StartupHydrationService>();
foreach (var module in modules)
{
services.AddSingleton(module);
module.RegisterServices(services, context.Configuration);
}
services.AddSingleton<PolyTraderSharp.Ui.ShellUiHost>();
services.AddTransient<PolyTraderSharp.Ui.LauncherForm>();
}).Build();
// State aus MySQL laden (Accounts/Trader/Settings), ohne die BackgroundServices zu starten.
try
{
host.Services.GetRequiredService<StartupHydrationService>()
.StartAsync(default).GetAwaiter().GetResult();
}
catch (Exception ex)
{
Console.WriteLine($"[WARN] Hydration übersprungen: {ex.Message}");
}
var uiHost = host.Services.GetRequiredService<PolyTraderSharp.Ui.ShellUiHost>();
foreach (var module in modules)
module.RegisterUi(uiHost, host.Services);
int failures = 0;
Console.WriteLine("=== Smoke-UI: View-Konstruktion ===");
foreach (var view in uiHost.Views)
{
try
{
using var form = view.CreateForm();
Console.WriteLine($"[OK] {view.Id} ({view.Title})");
}
catch (Exception ex)
{
failures++;
Console.WriteLine($"[FEHLER] {view.Id}: {ex.GetType().Name}: {ex.Message}");
}
}
try
{
using var launcher = host.Services.GetRequiredService<PolyTraderSharp.Ui.LauncherForm>();
Console.WriteLine("[OK] LauncherForm konstruiert");
}
catch (Exception ex)
{
failures++;
Console.WriteLine($"[FEHLER] LauncherForm: {ex.GetType().Name}: {ex.Message}");
}
Console.WriteLine(failures == 0 ? "=== Smoke-UI OK ===" : $"=== Smoke-UI: {failures} Fehler ===");
return failures;
}
} }