diff --git a/SVSim.UnitTests/Controllers/PackControllerFullCatalogTests.cs b/SVSim.UnitTests/Controllers/PackControllerFullCatalogTests.cs
index bcf2a24..30c1065 100644
--- a/SVSim.UnitTests/Controllers/PackControllerFullCatalogTests.cs
+++ b/SVSim.UnitTests/Controllers/PackControllerFullCatalogTests.cs
@@ -9,16 +9,16 @@ using SVSim.UnitTests.Infrastructure;
namespace SVSim.UnitTests.Controllers;
///
-/// Drives the importer + controller against the full production pack seed (35 packs). Guards
-/// against regressions in either layer caused by future seed refreshes.
+/// Drives the importer + controller against the full production pack seed. Guards against
+/// regressions in either layer caused by future seed refreshes.
///
public class PackControllerFullCatalogTests
{
[Test]
- public async Task Info_returns_full_35_pack_catalog_from_production_seed()
+ public async Task Info_round_trips_every_active_pack_from_production_seed()
{
// The production seed (packs.json) is overlaid by a 3-pack test fixture in the default test
- // output dir (see SVSim.UnitTests.csproj). For this test we need the FULL 35-pack catalog,
+ // output dir (see SVSim.UnitTests.csproj). For this test we need the FULL prod catalog,
// so we point PackImporter at a temp seed dir holding only the upstream production seed
// (copied from the Bootstrap project's source-tree Data/seeds/).
var prodSeed = LocateProdSeed("packs.json");
@@ -40,6 +40,15 @@ public class PackControllerFullCatalogTests
long viewerId = await factory.SeedViewerAsync();
+ // Snapshot the clock BEFORE the request so the expected count derives from the
+ // same moment the controller will read. PackController calls DateTime.UtcNow
+ // directly (not via TimeProvider), so we can't share a single instant — the
+ // sub-millisecond window between this and the controller's read is the only
+ // race exposure and any pack whose complete_date falls inside it is on the
+ // boundary either way.
+ var now = DateTime.UtcNow;
+ int expectedActive = CountActiveInSeed(prodSeed, now);
+
using var client = factory.CreateAuthenticatedClient(viewerId);
var response = await client.PostAsync(
"/pack/info",
@@ -51,23 +60,28 @@ public class PackControllerFullCatalogTests
using var doc = JsonDocument.Parse(body);
var list = doc.RootElement.GetProperty("pack_config_list");
- Assert.That(list.GetArrayLength(), Is.EqualTo(35),
- "Full prod seed should yield 35 active packs as of 2026-05-23.");
- // Spot-check pack 99047 (LegendCardPack throwback, pack_category=1)
- bool sawSpecial = false;
+ Assert.That(list.GetArrayLength(), Is.EqualTo(expectedActive),
+ $"Importer+controller must surface every active pack in the prod seed " +
+ $"(expected {expectedActive} active as of {now:O}).");
+
+ // Schema-fidelity spot check: at least one pack with a non-default pack_category
+ // proves the field survives the JSON round trip. Doesn't pin to a specific pack id
+ // so it stays valid as seasonal packs roll over.
+ bool sawNonDefaultCategory = false;
for (int i = 0; i < list.GetArrayLength(); i++)
{
- var el = list[i];
- if (el.GetProperty("parent_gacha_id").GetInt32() == 99047)
+ if (list[i].GetProperty("pack_category").GetInt32() != 0)
{
- Assert.That(el.GetProperty("pack_category").GetInt32(), Is.EqualTo(1),
- "99047 is a LegendCardPack (category 1) in the prod capture.");
- sawSpecial = true;
+ sawNonDefaultCategory = true;
break;
}
}
- Assert.That(sawSpecial, Is.True, "pack 99047 must be in the prod capture output.");
+ Assert.That(sawNonDefaultCategory, Is.True,
+ "At least one active pack should carry a non-default pack_category — " +
+ "the prod seed always has e.g. category 1 LegendCardPacks alongside the " +
+ "category 0 standard packs. If this fails, either the round-trip dropped " +
+ "the field or the seed no longer contains any non-default category.");
}
finally
{
@@ -75,6 +89,28 @@ public class PackControllerFullCatalogTests
}
}
+ ///
+ /// Mirrors PackRepository.GetActivePacks's filter (IsEnabled &&
+ /// CommenceDate <= now <= CompleteDate) against the raw seed file. is_enabled
+ /// defaults to true to match PackSeed.IsEnabled. Uses the same wire-date parser
+ /// as the importer so any date-string quirk is parsed identically on both sides.
+ ///
+ private static int CountActiveInSeed(string seedPath, DateTime when)
+ {
+ using var doc = JsonDocument.Parse(File.ReadAllText(seedPath));
+ int count = 0;
+ foreach (var pack in doc.RootElement.EnumerateArray())
+ {
+ bool enabled = !pack.TryGetProperty("is_enabled", out var en) || en.GetBoolean();
+ if (!enabled) continue;
+
+ var commence = ImporterBase.ParseWireDateTime(pack.GetProperty("commence_date").GetString());
+ var complete = ImporterBase.ParseWireDateTime(pack.GetProperty("complete_date").GetString());
+ if (commence <= when && complete >= when) count++;
+ }
+ return count;
+ }
+
///
/// The test output dir's Data/seeds/packs.json is the fixture overlay (3 packs). The
/// upstream production seed lives in the Bootstrap project's source tree. Walk up from the