using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using NeonSprawl.Server.Game.Mastery; using NeonSprawl.Server.Game.PositionState; using NeonSprawl.Server.Game.Skills; using NeonSprawl.Server.Tests.Game.PositionState; using Npgsql; using Xunit; namespace NeonSprawl.Server.Tests.Game.Mastery; [Collection("Postgres integration")] public sealed class PerkStatePersistenceIntegrationTests(PostgresIntegrationHarness harness) { private PostgresWebApplicationFactory Factory => harness.Factory; [RequirePostgresFact] public async Task BranchPickAndUnlock_ShouldPersistAcrossNewFactory() { // Arrange await ResetPerkTablesAsync(); const string playerId = "dev-local-1"; using (var scope = Factory.Services.CreateScope()) { var xpStore = scope.ServiceProvider.GetRequiredService(); var engine = scope.ServiceProvider.GetRequiredService(); Assert.True(xpStore.TryApplyXpDelta(playerId, "salvage", 450, out _, out _)); Assert.True(engine.TrySelectBranch(playerId, "salvage", 1, "scrap_efficiency").Success); _ = engine.ReevaluateAfterLevelUp(playerId, "salvage", newLevel: 4); } // Act PerkStateSnapshot snapshot; await using (var secondFactory = new PostgresWebApplicationFactory()) { using var scope = secondFactory.Services.CreateScope(); var perkStore = scope.ServiceProvider.GetRequiredService(); snapshot = perkStore.GetSnapshot(playerId); } // Assert Assert.Equal("scrap_efficiency", snapshot.BranchPicksBySkillId["salvage"][1]); Assert.Contains("salvage_scrap_efficiency_1", snapshot.UnlockedPerkIds); } private async Task ResetPerkTablesAsync() { var cs = Environment.GetEnvironmentVariable("ConnectionStrings__NeonSprawl"); if (string.IsNullOrWhiteSpace(cs)) { throw new InvalidOperationException("ConnectionStrings__NeonSprawl is not set."); } _ = Factory.Services; var options = Factory.Services.GetRequiredService>().Value; var positionDdlPath = Path.Combine(AppContext.BaseDirectory, "db", "migrations", "V001__player_position.sql"); var perkDdlPath = Path.Combine(AppContext.BaseDirectory, "db", "migrations", "V004__player_perk_state.sql"); if (!File.Exists(positionDdlPath) || !File.Exists(perkDdlPath)) { throw new FileNotFoundException("Test DDL for perk persistence not found."); } var positionDdl = await File.ReadAllTextAsync(positionDdlPath); var perkDdl = await File.ReadAllTextAsync(perkDdlPath); await using var conn = new NpgsqlConnection(cs); await conn.OpenAsync(); await using (var applyPosition = new NpgsqlCommand(positionDdl, conn)) { await applyPosition.ExecuteNonQueryAsync(); } await using (var truncate = new NpgsqlCommand("TRUNCATE player_position CASCADE;", conn)) { await truncate.ExecuteNonQueryAsync(); } PostgresPositionBootstrap.SeedDevPlayer(conn, options); await using (var applyPerk = new NpgsqlCommand(perkDdl, conn)) { await applyPerk.ExecuteNonQueryAsync(); } } }