From 3549b647a5b94d73df72cc307f6707341fdaeaad Mon Sep 17 00:00:00 2001 From: VinPropane Date: Sat, 27 Jun 2026 19:23:17 -0400 Subject: [PATCH] NEO-147: add standing and duplicate-seed review follow-up tests --- .../ContractGeneratorOperationsTests.cs | 59 ++++++++++++++++--- ...actGeneratorPersistenceIntegrationTests.cs | 16 ++--- 2 files changed, 58 insertions(+), 17 deletions(-) diff --git a/server/NeonSprawl.Server.Tests/Game/Contracts/ContractGeneratorOperationsTests.cs b/server/NeonSprawl.Server.Tests/Game/Contracts/ContractGeneratorOperationsTests.cs index 6243c05..e296fe0 100644 --- a/server/NeonSprawl.Server.Tests/Game/Contracts/ContractGeneratorOperationsTests.cs +++ b/server/NeonSprawl.Server.Tests/Game/Contracts/ContractGeneratorOperationsTests.cs @@ -3,6 +3,7 @@ using Microsoft.Extensions.Options; using NeonSprawl.Server.Game.Contracts; using NeonSprawl.Server.Game.Factions; using NeonSprawl.Server.Game.PositionState; +using NeonSprawl.Server.Game.Quests; using NeonSprawl.Server.Tests; using Xunit; @@ -15,6 +16,7 @@ public sealed class ContractGeneratorOperationsTests private const string TemplateId = "prototype_contract_clear_combat_pocket"; private const string EncounterId = "prototype_combat_pocket"; private const string GridOperatorsFactionId = "prototype_faction_grid_operators"; + private const string GatedTemplateId = "test_gated_contract_template"; private const string SeedBucket = "2026-06-22"; private const string AlternateSeedBucket = "2026-06-23"; private static readonly DateTimeOffset IssuedAt = new(2026, 6, 22, 10, 0, 0, TimeSpan.Zero); @@ -63,10 +65,10 @@ public sealed class ContractGeneratorOperationsTests public void MakeDeterministicInstanceId_ShouldMatchForSameInputs_AndDifferForDifferentSeedBucket() { // Arrange + // Act var first = ContractInstanceIds.MakeDeterministicInstanceId(PlayerId, TemplateId, SeedBucket); var same = ContractInstanceIds.MakeDeterministicInstanceId(PlayerId, TemplateId, SeedBucket); var different = ContractInstanceIds.MakeDeterministicInstanceId(PlayerId, TemplateId, AlternateSeedBucket); - // Act // Assert Assert.Equal(first, same); Assert.NotEqual(first, different); @@ -151,6 +153,48 @@ public sealed class ContractGeneratorOperationsTests Assert.Equal(ContractGeneratorReasonCodes.NoEligibleTemplate, result.ReasonCode); } + [Fact] + public void TryIssue_ShouldDenyNoEligibleTemplate_WhenFactionStandingBelowMin() + { + // Arrange + var prototype = PrototypeE7M4ContractCatalogRules.ExpectedTemplateFreeze[TemplateId]; + var gatedTemplate = prototype with + { + Id = GatedTemplateId, + MinFactionStanding = new FactionGateRuleRow(GridOperatorsFactionId, 1), + }; + var deps = CreateDependencies(gatedTemplate); + // Act + var result = TryIssue(deps, templateId: GatedTemplateId, seedBucket: SeedBucket); + // Assert + Assert.False(result.Success); + Assert.Equal(ContractGeneratorReasonCodes.NoEligibleTemplate, result.ReasonCode); + Assert.Null(result.Snapshot); + Assert.False(deps.InstanceStore.TryGetActiveForPlayer(PlayerId, out _)); + } + + [Fact] + public void TryIssue_ShouldDenyInvalidIds_WhenReissuingSameSeedAfterComplete() + { + // Arrange + var deps = CreateDependencies(); + var first = TryIssue(deps, templateId: TemplateId, seedBucket: SeedBucket); + Assert.True(first.Success); + Assert.True(deps.InstanceStore.TryMarkComplete( + PlayerId, + first.Snapshot!.ContractInstanceId, + CompletedAt, + out _)); + // Act + var second = TryIssue(deps, templateId: TemplateId, seedBucket: SeedBucket); + // Assert + Assert.False(second.Success); + Assert.Equal(ContractGeneratorReasonCodes.InvalidIds, second.ReasonCode); + Assert.NotNull(second.Snapshot); + Assert.Equal(ContractInstanceStatus.Completed, second.Snapshot!.Status); + Assert.False(deps.InstanceStore.TryGetActiveForPlayer(PlayerId, out _)); + } + [Fact] public void TryIssue_ShouldDenyPlayerNotWritable_WhenPlayerUnknown() { @@ -204,13 +248,14 @@ public sealed class ContractGeneratorOperationsTests deps.StandingStore, deps.TimeProvider); - private static GeneratorTestDependencies CreateDependencies() + private static GeneratorTestDependencies CreateDependencies(params ContractTemplateRow[] templates) { - var template = PrototypeE7M4ContractCatalogRules.ExpectedTemplateFreeze[TemplateId]; - var rows = new Dictionary(StringComparer.Ordinal) - { - [template.Id] = template, - }; + var rows = templates.Length == 0 + ? new Dictionary(StringComparer.Ordinal) + { + [TemplateId] = PrototypeE7M4ContractCatalogRules.ExpectedTemplateFreeze[TemplateId], + } + : templates.ToDictionary(static t => t.Id, StringComparer.Ordinal); var catalog = new ContractTemplateCatalog("/tmp/catalog", rows, catalogJsonFileCount: 1); var templateRegistry = new ContractTemplateRegistry(catalog); var positionOptions = Options.Create(new GamePositionOptions { DevPlayerId = PlayerId }); diff --git a/server/NeonSprawl.Server.Tests/Game/Contracts/ContractGeneratorPersistenceIntegrationTests.cs b/server/NeonSprawl.Server.Tests/Game/Contracts/ContractGeneratorPersistenceIntegrationTests.cs index 3d5a0c2..cdc7a2c 100644 --- a/server/NeonSprawl.Server.Tests/Game/Contracts/ContractGeneratorPersistenceIntegrationTests.cs +++ b/server/NeonSprawl.Server.Tests/Game/Contracts/ContractGeneratorPersistenceIntegrationTests.cs @@ -29,7 +29,7 @@ public sealed class ContractGeneratorPersistenceIntegrationTests(PostgresIntegra TemplateId, SeedBucket); - // Act — issue through first host + // Act ContractIssueOperationResult issued; using (var firstScope = Factory.Services.CreateScope()) { @@ -45,19 +45,15 @@ public sealed class ContractGeneratorPersistenceIntegrationTests(PostgresIntegra services.GetRequiredService()); } - ContractInstanceState readBack; - await using var secondFactory = new PostgresWebApplicationFactory(); - using var secondScope = secondFactory.Services.CreateScope(); - var readStore = secondScope.ServiceProvider.GetRequiredService(); - readBack = readStore.TryGetActiveForPlayer(PlayerId, out var snapshot) - ? snapshot - : null!; - // Assert Assert.True(issued.Success); Assert.Equal(expectedInstanceId, issued.Snapshot!.ContractInstanceId); Assert.Equal(EncounterId, issued.EncounterTemplateId); - Assert.NotNull(readBack); + + 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);