NEO-147: add standing and duplicate-seed review follow-up tests

pull/188/head
VinPropane 2026-06-27 19:23:17 -04:00
parent be0525b152
commit 3549b647a5
2 changed files with 58 additions and 17 deletions

View File

@ -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<string, ContractTemplateRow>(StringComparer.Ordinal)
var rows = templates.Length == 0
? new Dictionary<string, ContractTemplateRow>(StringComparer.Ordinal)
{
[template.Id] = template,
};
[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 });

View File

@ -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<TimeProvider>());
}
ContractInstanceState readBack;
await using var secondFactory = new PostgresWebApplicationFactory();
using var secondScope = secondFactory.Services.CreateScope();
var readStore = secondScope.ServiceProvider.GetRequiredService<IContractInstanceStore>();
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<IContractInstanceStore>();
Assert.True(readStore.TryGetActiveForPlayer(PlayerId, out var readBack));
Assert.Equal(expectedInstanceId, readBack.ContractInstanceId);
Assert.Equal(TemplateId, readBack.TemplateId);
Assert.Equal(ContractInstanceStatus.Active, readBack.Status);