using NeonSprawl.Server.Game.Contracts; using NeonSprawl.Server.Game.Factions; using NeonSprawl.Server.Game.PositionState; using NeonSprawl.Server.Tests.Game.PositionState; namespace NeonSprawl.Server.Tests.Game.Contracts; [Collection("Postgres integration")] public sealed class ContractGeneratorPersistenceIntegrationTests(PostgresIntegrationHarness harness) { private PostgresWebApplicationFactory Factory => harness.Factory; private const string PlayerId = "dev-local-1"; private const string TemplateId = "prototype_contract_clear_combat_pocket"; private const string EncounterId = "prototype_combat_pocket"; private const string SeedBucket = "2026-06-22"; [RequirePostgresFact] public async Task TryIssue_ShouldPersistActiveInstanceAcrossNewFactory() { // Arrange await ResetContractInstanceTableAsync(); var expectedInstanceId = ContractInstanceIds.MakeDeterministicInstanceId( PlayerId, TemplateId, SeedBucket); // Act ContractIssueOperationResult issued; using (var firstScope = Factory.Services.CreateScope()) { var services = firstScope.ServiceProvider; issued = ContractGeneratorOperations.TryIssue( PlayerId, TemplateId, SeedBucket, zoneDifficultyBand: null, services.GetRequiredService(), services.GetRequiredService(), services.GetRequiredService(), services.GetRequiredService()); } // Assert Assert.True(issued.Success); Assert.Equal(expectedInstanceId, issued.Snapshot!.ContractInstanceId); Assert.Equal(EncounterId, issued.EncounterTemplateId); await using var secondFactory = new PostgresWebApplicationFactory(); using var secondScope = secondFactory.Services.CreateScope(); var readStore = secondScope.ServiceProvider.GetRequiredService(); Assert.True(readStore.TryGetActiveForPlayer(PlayerId, out var readBack)); Assert.Equal(expectedInstanceId, readBack.ContractInstanceId); Assert.Equal(TemplateId, readBack.TemplateId); Assert.Equal(ContractInstanceStatus.Active, readBack.Status); Assert.Equal(SeedBucket, readBack.SeedBucket); } private async Task ResetContractInstanceTableAsync() { 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 instanceDdlPath = Path.Combine(AppContext.BaseDirectory, "db", "migrations", "V011__contract_instance.sql"); if (!File.Exists(positionDdlPath) || !File.Exists(instanceDdlPath)) { throw new FileNotFoundException("Test DDL for contract generator persistence not found."); } var positionDdl = await File.ReadAllTextAsync(positionDdlPath); var instanceDdl = await File.ReadAllTextAsync(instanceDdlPath); 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 applyInstance = new NpgsqlCommand(instanceDdl, conn); await applyInstance.ExecuteNonQueryAsync(); } }