The plugin has a focused unit-test suite (59 tests as of 2026-08-02) over the pure logic that can run outside the
game — the registry/parse/era layer, the reflection compatibility report (GameBinding), and the in-game smoke
harness’s verdict (SmokeVerdict). It is a deliberate, bounded suite, not a coverage target: it guards the functions
where bugs have actually hidden, and stops there on purpose.
dotnet test Tests/HumankindAssetFramework.Tests.csproj -c Release
| Function | Lives in | What’s asserted |
|---|---|---|
ParseModels |
UniversalInjectPatch.cs |
JSON→ModelEntry field mapping; defaults (animPhaseSpread 0.5, scale/brightness 1); signed GUID components; per-object isolation (an omitted field doesn’t shift onto another model); robustness — garbage/empty input → empty without throwing; the regex fallback recovery when JObject.Parse rejects the document (keys entry count on Min(pawnDescription, skel, atlas)) |
ResolvePacks |
UniversalInjectPatch.cs |
duplicate-modId reject (first file kept); dependsOn/loadAfter ordering; missing-dep skip + transitive strand (fixpoint); cycle → file-order + note; soft loadAfter to an absent modId; stable seed order (the invariant that keeps today’s single-pack setup byte-identical) |
LongestMatch |
UniversalInjectPatch.cs |
most-specific substring wins (not first-in-order); single-match fallback; no-match → null |
RegexStrArray |
UniversalInjectPatch.cs |
wrapper string-array extraction; empty-item filtering; missing field → empty |
CoreDesc |
UniversalInject.Combat.cs |
trailing _NN variant-suffix strip |
GuidToLong |
UniversalInject.Combat.cs |
null / non-numeric → 0; numeric string parses |
EraFromName |
UniversalInject.ScaleEra.cs |
extract EraN (case-insensitive, multi-digit); none/null → −1 |
EraAnchorFor |
UniversalInject.ScaleEra.cs |
the Global Era Lab anchor rule — a unit stays at 1.0 unless an authored grid cell says otherwise (own-age-or-earlier → 1.0; later-but-unauthored → 1.0; non-positive eras clamp cleanly) |
GameBinding.Validate / Cached |
Patches/GameBinding.cs |
the startup reflection compatibility report — type/member resolution, incl. the simple-name (Type.Name) fallback scan; a game-update rename is reported, not silently absorbed |
SmokeVerdict |
Patches/UniversalInject.SmokeTest.cs |
the in-game smoke harness’s PASS/FAIL rule — PASS iff every catalogued binding resolved, zero injection errors, and the registry loaded ≥1 model; each fail reason surfaced; repointed-zero still passes (no units on the map isn’t a failure) |
These map directly to the registry bugs this codebase has actually hit — the ParseGuidCsv sign bug, LongestMatch
ambiguity, “wrapper-parse drops overrides”, the substring pawn-match — so the suite is a regression net, not
coverage theatre.
net471 (matches the plugin), one test project Tests/HumankindAssetFramework.Tests.csproj.internal, exposed to the test assembly via [InternalsVisibleTo]
(Properties/AssemblyInfo.cs). A few were bumped private→internal purely for this; none were made public.Plugin.Log: null outside the game, so each test class’s ctor sets Plugin.Log = new ManualLogSource("test")
(a listener-less source → every LogXxx is a safe no-op).References\ DLLs as the plugin build; the test project mirrors them
into its own bin so the plugin assembly’s deps resolve at runtime. Tests\** is excluded from the plugin’s compile
globs so the xUnit files never leak into the plugin build.This boundary is intentional. Adding tests past it would be green ceremony that guards nothing real.
RunSmokeTest) that asserts the plugin came up and injected cleanly and logs one PASS/FAIL line. That’s the
right instrument for this half: a human loads a game, the harness does the checking. Its verdict logic is pure
and unit-tested (SmokeVerdict, above); only the genuinely untestable part — gathering the live numbers via
reflection — runs in-game.ParseGuidCsv, MakeGuid, EmitterName — build/consume Amplitude types via reflection, absent in the test host.FindEntryForUnitDefinition — delegates to the already-tested LongestMatch + CoreDesc; testing it would mean
exposing the entries global as a test seam for ~zero new coverage.StrList, SanitizeFile, one-line accessors) — too trivial to regress meaningfully.private, bump it to internal (never public just for tests) — [InternalsVisibleTo] handles
the rest.Plugin.Log in the fixture if the code under test logs.See also: docs/Building.md (build/run), docs/Code-Map.md (where the tested functions live),
docs/Framework-Review.md (the dated changelog of what each test batch added).