docs/concepts: one concept per engine system (how it works, invariants and traps, tests, what the review fixed), with an index. docs/analysis/ rewrite-proposals.md: the structural causes behind the review's defects and how to replace them, with reasons, costs and an order; analysis only. AGENTS.md points to both and lists the mods with unit tests.
3.1 KiB
Testing infrastructure (tests/, Core/Mods/Testing, mod test_suites)
Four layers, from the cheapest to the most complete. tests/Cthangover.TestRunner runs all of them
as one command: build, then xUnit, then every integration test of tests/tests.manifest.json, then a
summary and a JSON report.
| Layer | Where | Runs in | Use for |
|---|---|---|---|
| xUnit | tests/Cthangover.Tests |
plain .NET, no Godot (Godot calls must be guarded by GameEnvironment.IsRuntime) |
pure core logic: pipeline, resolvers, patches, saves, time, DSL parsing |
| Mod unit tests | mods/<mod>/tests via test_sources, [ModTest("group/case")] + ModAssert |
the real game process (--run-tests [--mod=]), compiled into <mod>__tests against the mod and its dependencies |
mod code that needs Godot or mod types: dialog runtime, inventory, quests, status effects, map, audio |
| Integration scenes | tests/Cthangover.Tests.Integration (dev build), one setup node per scene printing [TEST_RESULT] |
a headless (or xvfb) Godot process per test | core runtime behaviour: event bus bridge, scene context, saves, coverage guards |
| Mod test suites | mods/<host>/test_suites/<id> (virtual mod <host>__<id>, mounted by activate args) |
full game, driven like a player | GUI flows: menu, inventory, hot reload, studio, tools, battles, dialog autoplay |
Mechanics worth knowing
-
Mod unit tests. Each test runs, then the runner recovers state:
IResettables reset;- registries rebuild;
- root nodes the test added are removed (removing a core node is an error);
Engine.TimeScaleis restored;- the per-mod sandbox is wiped.
It then checks invariants. Async tests can await frames, and
TimeoutSecondsapplies to them. The report goes touser://mod_tests/last-report.json. Mods with tests: cooking, locale_atlas, dialog_base, items_base, quests_base, turn_battle_base, map_base, audio_base. -
Coverage guards.
coverage(battle cores) andcoverage_subsystems(migrated subsystems) fail when a registered core or subsystem has no declared test, or when the declared test is not intests.manifest.json. -
Dialog autoplay (
dialog_base__autoplay):--choices=/--timeout=mount a driver that clicks through dialogs;- by default it passes when the box goes idle;
--dialogs=Npasses after N finished dialogs instead (hub games never go idle).
-
Runner conventions. It adds
--all-modsto tests without a game, and adds--no-hot-reloadunless a test asks for--hot-reload. It checks scene path case before Godot starts. Logs go totests/artifacts/logs/<name>.log. -
Speed. A full run takes about 12 minutes on the 9p workspace. Most of each integration test's time is the game boot; mod compilation is cached and recompiles after every core rebuild.
Rules (skill-code / skill-test)
- Test the public contract, check data rather than calls, and run JSON round trips for saved types.
- Use reflection only to set up static singletons.
- A failing test is never weakened. When it fails for a reason other than your change, ask.
- Read the logs: every
Errorline is a red flag even when the test passes.