From e5f00fa6936b8f487cdc3b3aac8841650b3accdd Mon Sep 17 00:00:00 2001 From: Frank Tovar Date: Fri, 3 Apr 2026 01:15:09 +0200 Subject: [PATCH] Add Rolemaster payload budget coverage --- README.md | 2 +- RpgRoller.Tests/PayloadBudgetTests.cs | 68 +++++++++++++++++++++++++++ 2 files changed, 69 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 988dbcb..ec9d5d8 100644 --- a/README.md +++ b/README.md @@ -138,7 +138,7 @@ dotnet dotnet-ef migrations add --project RpgRoller/RpgRoller.cs ```powershell dotnet test RpgRoller.Tests/RpgRoller.Tests.csproj --collect:"XPlat Code Coverage" --settings RpgRoller.Tests/coverlet.runsettings ``` -- Regression tests enforce payload budgets for the hottest contracts: character sheet reads, initial log page loads, incremental log updates, and roll mutation responses. +- Regression tests enforce payload budgets for the hottest contracts: character sheet reads, initial log page loads, incremental log updates, roll mutation responses, and lazy-loaded Rolemaster roll detail payloads. - Coverage gate: ```powershell pwsh ./scripts/check-coverage.ps1 -MinLineRate 0.90 -MinBranchRate 0.70 diff --git a/RpgRoller.Tests/PayloadBudgetTests.cs b/RpgRoller.Tests/PayloadBudgetTests.cs index 24642b3..3216e3d 100644 --- a/RpgRoller.Tests/PayloadBudgetTests.cs +++ b/RpgRoller.Tests/PayloadBudgetTests.cs @@ -87,6 +87,29 @@ public sealed class PayloadBudgetTests AssertPayloadWithinBudget(incrementalPage, 2 * 1024, "incremental log update"); } + [Fact] + public void RolemasterCampaignLogInitialPagePayload_StaysWithinBudget() + { + using var harness = ServiceTestSupport.CreateHarness(CreateRolemasterOpenEndedRolls(90)); + var service = harness.Service; + + service.Register("gm-rm-log-budget", "Password123", "GM"); + service.Register("owner-rm-log-budget", "Password123", "Owner"); + + var gmSession = ServiceTestSupport.GetValue(service.Login("gm-rm-log-budget", "Password123")).SessionToken; + var ownerSession = ServiceTestSupport.GetValue(service.Login("owner-rm-log-budget", "Password123")).SessionToken; + + var campaign = ServiceTestSupport.GetValue(service.CreateCampaign(gmSession, "Rolemaster Payload Log", "rolemaster")); + var character = ServiceTestSupport.GetValue(service.CreateCharacter(ownerSession, "Open Hero", campaign.Id)); + var skill = ServiceTestSupport.GetValue(service.CreateSkill(ownerSession, character.Id, "Awareness", "d100!+85", 0, false, null, 5)); + + for (var i = 0; i < 25; i++) + _ = ServiceTestSupport.GetValue(service.RollSkill(ownerSession, skill.Id, "public")); + + var page = ServiceTestSupport.GetValue(service.GetCampaignLogPage(gmSession, campaign.Id, limit: 25)); + AssertPayloadWithinBudget(page, 8 * 1024, "initial rolemaster log page"); + } + [Fact] public void RollResultPayload_StaysWithinJsInteropBudget() { @@ -107,6 +130,41 @@ public sealed class PayloadBudgetTests AssertPayloadWithinBudget(roll, 16 * 1024, "roll mutation response"); } + [Fact] + public void RolemasterRollDetailPayload_StaysWithinBudget_AndRolemasterMetadataRemainsLazy() + { + using var harness = ServiceTestSupport.CreateHarness([96, 100, 100, 100, 100, 97, 12]); + var service = harness.Service; + + service.Register("gm-rm-detail-budget", "Password123", "GM"); + service.Register("owner-rm-detail-budget", "Password123", "Owner"); + + var gmSession = ServiceTestSupport.GetValue(service.Login("gm-rm-detail-budget", "Password123")).SessionToken; + var ownerSession = ServiceTestSupport.GetValue(service.Login("owner-rm-detail-budget", "Password123")).SessionToken; + + var campaign = ServiceTestSupport.GetValue(service.CreateCampaign(gmSession, "Rolemaster Payload Detail", "rolemaster")); + var character = ServiceTestSupport.GetValue(service.CreateCharacter(ownerSession, "Detail Hero", campaign.Id)); + var skill = ServiceTestSupport.GetValue(service.CreateSkill(ownerSession, character.Id, "Sense", "d100!+85", 0, false, null, 5)); + + var roll = ServiceTestSupport.GetValue(service.RollSkill(ownerSession, skill.Id, "public")); + var logPage = ServiceTestSupport.GetValue(service.GetCampaignLogPage(gmSession, campaign.Id, limit: 5)); + var detail = ServiceTestSupport.GetValue(service.GetRollDetail(gmSession, roll.RollId)); + + AssertPayloadWithinBudget(detail, 4 * 1024, "rolemaster roll detail"); + + var rollJson = JsonSerializer.Serialize(roll, SerializerOptions); + var logPageJson = JsonSerializer.Serialize(logPage, SerializerOptions); + var detailJson = JsonSerializer.Serialize(detail, SerializerOptions); + + Assert.DoesNotContain("\"signedContribution\":null", rollJson, StringComparison.Ordinal); + Assert.DoesNotContain("\"signedContribution\"", logPageJson, StringComparison.Ordinal); + Assert.DoesNotContain("\"sequence\"", logPageJson, StringComparison.Ordinal); + Assert.DoesNotContain("\"breakdown\"", logPageJson, StringComparison.Ordinal); + Assert.Contains("\"kind\":\"rolemaster-open-ended-initial\"", detailJson, StringComparison.Ordinal); + Assert.Contains("\"signedContribution\":96", detailJson, StringComparison.Ordinal); + Assert.Contains("\"sequence\":6", detailJson, StringComparison.Ordinal); + } + private static void AssertPayloadWithinBudget(T payload, int maxBytes, string label) { var byteCount = JsonSerializer.SerializeToUtf8Bytes(payload, SerializerOptions).Length; @@ -123,5 +181,15 @@ public sealed class PayloadBudgetTests return scriptedRolls; } + private static int[] CreateRolemasterOpenEndedRolls(int count) + { + var values = new[] { 96, 100, 12 }; + var scriptedRolls = new int[count]; + for (var i = 0; i < count; i++) + scriptedRolls[i] = values[i % values.Length]; + + return scriptedRolls; + } + private static readonly JsonSerializerOptions SerializerOptions = RpgRollerJson.CreateSerializerOptions(); }