NEO-48: revert WhenWritingNull on skill-progression grant API

Keep omit-null reasonCode only on new perk-state responses; restoring
the existing grant endpoint wire format (reasonCode: null on success).
pull/83/head
VinPropane 2026-05-17 20:59:37 -04:00
parent 12e52bccc9
commit 00c5937872
3 changed files with 23 additions and 1 deletions

View File

@ -8,6 +8,7 @@ docs {
NEO-38: single skill XP grant + allowlist + level-up metadata; see server README. NEO-38: single skill XP grant + allowlist + level-up metadata; see server README.
NEO-40: same contract; server adds comment-only telemetry hook markers (no request/response change). NEO-40: same contract; server adds comment-only telemetry hook markers (no request/response change).
NEO-47: on level-up, server re-evaluates mastery perk unlocks internally (no JSON change until NEO-48 perk-state HTTP). NEO-47: on level-up, server re-evaluates mastery perk unlocks internally (no JSON change until NEO-48 perk-state HTTP).
Success responses include reasonCode: null (stable since NEO-38); perk-state POST omits reasonCode on success instead.
E2.M2 tracking row in docs/decomposition/modules/documentation_and_implementation_alignment.md lists NEO-40 landed with plan + manual QA links. E2.M2 tracking row in docs/decomposition/modules/documentation_and_implementation_alignment.md lists NEO-40 landed with plan + manual QA links.
} }
@ -32,6 +33,7 @@ tests {
const body = res.getBody(); const body = res.getBody();
expect(body.schemaVersion).to.equal(1); expect(body.schemaVersion).to.equal(1);
expect(body.granted).to.equal(true); expect(body.granted).to.equal(true);
expect(body.reasonCode).to.equal(null);
expect(body.progression).to.be.an("object"); expect(body.progression).to.be.an("object");
expect(body.progression.schemaVersion).to.equal(1); expect(body.progression.schemaVersion).to.equal(1);
expect(body.progression.playerId).to.equal("dev-local-1"); expect(body.progression.playerId).to.equal("dev-local-1");

View File

@ -1,6 +1,7 @@
using System.Linq; using System.Linq;
using System.Net; using System.Net;
using System.Net.Http.Json; using System.Net.Http.Json;
using System.Text.Json;
using NeonSprawl.Server.Game.Skills; using NeonSprawl.Server.Game.Skills;
using Xunit; using Xunit;
@ -120,6 +121,26 @@ public sealed class SkillProgressionGrantApiTests
Assert.Equal(SkillProgressionSnapshotApi.ReasonInvalidAmount, envelope.ReasonCode); Assert.Equal(SkillProgressionSnapshotApi.ReasonInvalidAmount, envelope.ReasonCode);
} }
[Fact]
public async Task PostSkillProgression_ShouldSerializeReasonCodeNull_OnSuccess()
{
// Arrange
await using var factory = new InMemoryWebApplicationFactory();
var client = factory.CreateClient();
// Act
var response = await client.PostAsJsonAsync(
"/game/players/dev-local-1/skill-progression",
ValidGrant("salvage", amount: 10));
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var json = await response.Content.ReadAsStringAsync();
using var doc = JsonDocument.Parse(json);
Assert.True(doc.RootElement.TryGetProperty("reasonCode", out var reasonCode));
Assert.Equal(JsonValueKind.Null, reasonCode.ValueKind);
}
[Fact] [Fact]
public async Task PostSkillProgression_ShouldApplyGrant_AndGetMatches_WhenTrainerAllowedForRefine() public async Task PostSkillProgression_ShouldApplyGrant_AndGetMatches_WhenTrainerAllowedForRefine()
{ {

View File

@ -34,7 +34,6 @@ public sealed class SkillProgressionGrantResponse
/// <remarks>Set when <see cref="Granted"/> is <c>false</c>; stable snake_case values (see README).</remarks> /// <remarks>Set when <see cref="Granted"/> is <c>false</c>; stable snake_case values (see README).</remarks>
[JsonPropertyName("reasonCode")] [JsonPropertyName("reasonCode")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? ReasonCode { get; init; } public string? ReasonCode { get; init; }
[JsonPropertyName("progression")] [JsonPropertyName("progression")]