NEO-70: Emit explicit nulls on CraftResponse optional fields.

Serialize reasonCode and xpGrantSummary as null when absent so Bruno
deny tests match inventory POST wire shape; assert JSON nulls in API tests.
pull/104/head
VinPropane 2026-05-24 18:13:10 -04:00
parent 2221dc6b4b
commit 87c27e231b
4 changed files with 7 additions and 4 deletions

View File

@ -126,7 +126,7 @@ tests {
const body = res.getBody(); const body = res.getBody();
expect(body.schemaVersion).to.equal(1); expect(body.schemaVersion).to.equal(1);
expect(body.success).to.equal(true); expect(body.success).to.equal(true);
expect(body.reasonCode).to.be.undefined; expect(body.reasonCode).to.equal(null);
expect(body.outputsGranted).to.deep.include({ itemId: "field_stim_mk0", quantity: 1 }); expect(body.outputsGranted).to.deep.include({ itemId: "field_stim_mk0", quantity: 1 });
expect(body.xpGrantSummary).to.be.an("object"); expect(body.xpGrantSummary).to.be.an("object");
expect(body.xpGrantSummary.skillId).to.equal("refine"); expect(body.xpGrantSummary.skillId).to.equal("refine");

View File

@ -3,5 +3,5 @@ meta {
} }
docs { docs {
NEO-70 craft HTTP. Run order within folder: deny requests (seq 14) then spine (seq 5). Full collection runs after dotnet test on shared Postgres — insufficient-materials clears refined stock; spine reuses persisted scrap and tolerates node_depleted gathers. Deny setup requests may use inventory POST only where noted. NEO-70 craft HTTP. CraftResponse emits explicit null for reasonCode / xpGrantSummary when absent (mirror inventory POST). Run order within folder: deny requests (seq 14) then spine (seq 5). Full collection runs after dotnet test on shared Postgres — insufficient-materials clears refined stock; spine reuses persisted scrap and tolerates node_depleted gathers. Deny setup requests may use inventory POST only where noted.
} }

View File

@ -104,6 +104,8 @@ public sealed class PlayerCraftApiTests
Assert.Equal("refine", body.XpGrantSummary!.SkillId); Assert.Equal("refine", body.XpGrantSummary!.SkillId);
Assert.Equal(RefineSkillXpConstants.ActivityXpPerCraftRefineCompletion, body.XpGrantSummary.Amount); Assert.Equal(RefineSkillXpConstants.ActivityXpPerCraftRefineCompletion, body.XpGrantSummary.Amount);
Assert.Equal(RefineSkillXpConstants.ActivitySourceKind, body.XpGrantSummary.SourceKind); Assert.Equal(RefineSkillXpConstants.ActivitySourceKind, body.XpGrantSummary.SourceKind);
var json = await response.Content.ReadAsStringAsync();
Assert.Contains("\"reasonCode\":null", json, StringComparison.Ordinal);
} }
[Fact] [Fact]
@ -179,6 +181,9 @@ public sealed class PlayerCraftApiTests
Assert.Empty(body.InputsConsumed); Assert.Empty(body.InputsConsumed);
Assert.Empty(body.OutputsGranted); Assert.Empty(body.OutputsGranted);
Assert.Null(body.XpGrantSummary); Assert.Null(body.XpGrantSummary);
var json = await response.Content.ReadAsStringAsync();
Assert.Contains("\"reasonCode\":\"unknown_recipe\"", json, StringComparison.Ordinal);
Assert.Contains("\"xpGrantSummary\":null", json, StringComparison.Ordinal);
} }
[Fact] [Fact]

View File

@ -30,7 +30,6 @@ public sealed class CraftResponse
public bool Success { get; init; } public bool Success { get; init; }
[JsonPropertyName("reasonCode")] [JsonPropertyName("reasonCode")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? ReasonCode { get; init; } public string? ReasonCode { get; init; }
[JsonPropertyName("inputsConsumed")] [JsonPropertyName("inputsConsumed")]
@ -40,7 +39,6 @@ public sealed class CraftResponse
public required IReadOnlyList<CraftIoAppliedJson> OutputsGranted { get; init; } public required IReadOnlyList<CraftIoAppliedJson> OutputsGranted { get; init; }
[JsonPropertyName("xpGrantSummary")] [JsonPropertyName("xpGrantSummary")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public CraftXpGrantSummaryJson? XpGrantSummary { get; init; } public CraftXpGrantSummaryJson? XpGrantSummary { get; init; }
} }