HumankindAssetFramework

Testing

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

What it covers

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.

How it’s wired

What is deliberately NOT unit-tested — and why

This boundary is intentional. Adding tests past it would be green ceremony that guards nothing real.

Adding a test

  1. If the target is private, bump it to internal (never public just for tests) — [InternalsVisibleTo] handles the rest.
  2. Only test pure logic (string/JSON/data in → data out). If it reflects into Amplitude/Unity, it belongs to the Phase-5 in-game seam, not here.
  3. Set Plugin.Log in the fixture if the code under test logs.
  4. Prefer tests that pin a real invariant or a historic bug, not line coverage.

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).