NEO-147: guard DenyCreateFailure when create snapshot is null
parent
81f4cde673
commit
c237bead53
|
|
@ -70,7 +70,7 @@ Issue a validated **`active`** contract instance from **`ContractSeed`** inputs
|
||||||
|
|
||||||
- **Types:** `ContractGeneratorReasonCodes`, `ContractIssueOperationResult`, `ContractInstanceIds.MakeDeterministicInstanceId`.
|
- **Types:** `ContractGeneratorReasonCodes`, `ContractIssueOperationResult`, `ContractInstanceIds.MakeDeterministicInstanceId`.
|
||||||
- **Orchestrator:** `ContractGeneratorOperations.TryIssue` — band + faction template selection, deterministic id, store persist.
|
- **Orchestrator:** `ContractGeneratorOperations.TryIssue` — band + faction template selection, deterministic id, store persist.
|
||||||
- **Tests:** `ContractGeneratorOperationsTests` (13 unit), `ContractGeneratorOperationsIntegrationTests` (DI), `ContractGeneratorPersistenceIntegrationTests` (`RequirePostgresFact`); **890** tests green.
|
- **Tests:** `ContractGeneratorOperationsTests` (14 unit), `ContractGeneratorOperationsIntegrationTests` (DI), `ContractGeneratorPersistenceIntegrationTests` (`RequirePostgresFact`); **891** tests green.
|
||||||
- **Docs:** `server/README.md` contract generator section; `E7_M4_ContractMissionGenerator.md` NEO-147 note.
|
- **Docs:** `server/README.md` contract generator section; `E7_M4_ContractMissionGenerator.md` NEO-147 note.
|
||||||
|
|
||||||
## Technical approach
|
## Technical approach
|
||||||
|
|
|
||||||
|
|
@ -195,6 +195,30 @@ public sealed class ContractGeneratorOperationsTests
|
||||||
Assert.False(deps.InstanceStore.TryGetActiveForPlayer(PlayerId, out _));
|
Assert.False(deps.InstanceStore.TryGetActiveForPlayer(PlayerId, out _));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void TryIssue_ShouldDenyInvalidIds_WhenCreateActiveReturnsFalseWithoutSnapshot()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var deps = CreateDependencies();
|
||||||
|
var store = new CreateActiveNullSnapshotStore(deps.InstanceStore);
|
||||||
|
var templateRegistry = deps.TemplateRegistry;
|
||||||
|
var standingStore = deps.StandingStore;
|
||||||
|
// Act
|
||||||
|
var result = ContractGeneratorOperations.TryIssue(
|
||||||
|
PlayerId,
|
||||||
|
TemplateId,
|
||||||
|
SeedBucket,
|
||||||
|
zoneDifficultyBand: null,
|
||||||
|
templateRegistry,
|
||||||
|
store,
|
||||||
|
standingStore,
|
||||||
|
deps.TimeProvider);
|
||||||
|
// Assert
|
||||||
|
Assert.False(result.Success);
|
||||||
|
Assert.Equal(ContractGeneratorReasonCodes.InvalidIds, result.ReasonCode);
|
||||||
|
Assert.Null(result.Snapshot);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void TryIssue_ShouldDenyPlayerNotWritable_WhenPlayerUnknown()
|
public void TryIssue_ShouldDenyPlayerNotWritable_WhenPlayerUnknown()
|
||||||
{
|
{
|
||||||
|
|
@ -290,4 +314,38 @@ public sealed class ContractGeneratorOperationsTests
|
||||||
IContractInstanceStore InstanceStore,
|
IContractInstanceStore InstanceStore,
|
||||||
IFactionStandingStore StandingStore,
|
IFactionStandingStore StandingStore,
|
||||||
TimeProvider TimeProvider);
|
TimeProvider TimeProvider);
|
||||||
|
|
||||||
|
/// <summary>Simulates store create failure without populating the out snapshot (Bugbot regression).</summary>
|
||||||
|
private sealed class CreateActiveNullSnapshotStore(IContractInstanceStore inner) : IContractInstanceStore
|
||||||
|
{
|
||||||
|
public bool CanWritePlayer(string playerId) => inner.CanWritePlayer(playerId);
|
||||||
|
|
||||||
|
public bool TryGet(string playerId, string contractInstanceId, out ContractInstanceState snapshot) =>
|
||||||
|
inner.TryGet(playerId, contractInstanceId, out snapshot);
|
||||||
|
|
||||||
|
public bool TryGetActiveForPlayer(string playerId, out ContractInstanceState snapshot) =>
|
||||||
|
inner.TryGetActiveForPlayer(playerId, out snapshot);
|
||||||
|
|
||||||
|
public bool TryCreateActive(
|
||||||
|
string playerId,
|
||||||
|
string contractInstanceId,
|
||||||
|
string templateId,
|
||||||
|
string seedBucket,
|
||||||
|
DateTimeOffset issuedAt,
|
||||||
|
out ContractInstanceState snapshot)
|
||||||
|
{
|
||||||
|
snapshot = null!;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TryMarkComplete(
|
||||||
|
string playerId,
|
||||||
|
string contractInstanceId,
|
||||||
|
DateTimeOffset completedAt,
|
||||||
|
out ContractInstanceState snapshot) =>
|
||||||
|
inner.TryMarkComplete(playerId, contractInstanceId, completedAt, out snapshot);
|
||||||
|
|
||||||
|
public bool TryClearInstance(string playerId, string contractInstanceId) =>
|
||||||
|
inner.TryClearInstance(playerId, contractInstanceId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -142,14 +142,14 @@ public static class ContractGeneratorOperations
|
||||||
private static ContractIssueOperationResult DenyCreateFailure(
|
private static ContractIssueOperationResult DenyCreateFailure(
|
||||||
IContractInstanceStore instanceStore,
|
IContractInstanceStore instanceStore,
|
||||||
string normalizedPlayerId,
|
string normalizedPlayerId,
|
||||||
ContractInstanceState attemptSnapshot)
|
ContractInstanceState? attemptSnapshot)
|
||||||
{
|
{
|
||||||
if (instanceStore.TryGetActiveForPlayer(normalizedPlayerId, out var active))
|
if (instanceStore.TryGetActiveForPlayer(normalizedPlayerId, out var active))
|
||||||
{
|
{
|
||||||
return Deny(ContractGeneratorReasonCodes.ActiveContractExists, active);
|
return Deny(ContractGeneratorReasonCodes.ActiveContractExists, active);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (attemptSnapshot.Status == ContractInstanceStatus.Active)
|
if (attemptSnapshot is { Status: ContractInstanceStatus.Active })
|
||||||
{
|
{
|
||||||
return Deny(ContractGeneratorReasonCodes.ActiveContractExists, attemptSnapshot);
|
return Deny(ContractGeneratorReasonCodes.ActiveContractExists, attemptSnapshot);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue