| name | ck-game-testing-discipline |
| description | Use when choosing coverage and test layers for CkFoundation gameplay, structuring AutoTests, preventing shared-world contamination, or defining proof of no regressions. |
ck-game-testing-discipline
Overview
This skill is the consumer testing policy for a game built on CkFoundation: what every
feature must cover, which test layer to pick, how a new game wires the test surfaces in,
the contamination traps unique to a shared suite world, and what counts as evidence.
The mechanics of authoring and running every layer — base-class API, wrapper generation,
populator pipeline, net stubs, gym registration, Gauntlet bridge, exact command lines — are
owned by CkTests' ck-tests-authoring-and-running skill and the three spec files in
Plugins/CkTests/Script/Common/. This skill cites them constantly and restates nothing.
Vocabulary (defined once): an AutoTest is an AngelScript entity script subclassing
UCk_AutoTest_Base that runs headless assertions inside one shared PIE world; a gym is
an interactive station level for eyeballing/tuning a feature (no assertions); a Gauntlet
test boots a real game process and asserts on log breadcrumbs; a driver is the
world-owning controller entity that discovers and routes to feature entities (see
ck-game-driver-architecture).
Evidence base: the BusterBlock corpus at superproject 52a75e13d — 216 AutoTest .as files
across 66 feature dirs, ~80 gym files across 23 features, 22 registered Gauntlet tests,
zero C++ unit tests (counted 2026-07-03). Corpus examples are labeled; everything else is
generic to any <Game> on the framework.
When NOT to use this skill
| If you need… | Load instead |
|---|
| Authoring/running mechanics for any layer (file template, populator, Session Frontend, net stubs, gym exec commands, Gauntlet bridge/exit codes, exact command lines) | ck-tests-authoring-and-running + the spec chain in Plugins/CkTests/Script/Common/ |
| Diagnosing a red or flaky test | ck-game-debugging-playbook |
| Writing the feature the test covers | ck-game-feature-recipe |
| The replicated-spawn ownership RULE and rationale | ck-game-replication-patterns (this skill owns only the test-side mechanics) |
| Cook/stage/package flow (staging test AS, NeverCook, DisablePlugins mechanics) | ck-game-build-and-cook |
Creating the <Game>Tests plugin skeleton from an empty repo | ck-game-project-bootstrap |
| Testing changes to the Ck plugins themselves | ck-change-control (framework skill) |
1. The coverage norm: feature and tests, same commit
Every behavioral feature lands with its AutoTests in the same commit as the behavior they
cover. Feature-with-test — not test-first, not test-after. [PROMOTED FROM CORPUS 2026-07-03] — this is uniform practice across the corpus, verified in the commit log:
01300d524 feat(employee): spawn EmployeeManager on StoreDriver + daily-wage debit (routed) + AutoTest — integration code + a new 174-line end-to-end AutoTest, one commit.
9a6bfb897 feat(employee): DayCycle shift reconcile -> OnShift/OffShift presence + AutoTest — behavior file + 118-line AutoTest, one commit.
0ddfada98, c27dfc88a — same pattern at the unit and compose tiers (below).
(All four verified via git show --stat 2026-07-03.)
1.1 The three-slice coverage shape
A feature built along the standard arc (ck-game-feature-recipe) accumulates three kinds of
AutoTest, each landing with the slice it covers. Corpus example (BusterBlock Employee):
| Slice | Covers | Lands with | Corpus exhibit |
|---|
| Unit-style AutoTest | the pure-logic namespace (no world state beyond the harness) | the logic file | 0ddfada98: BB_Employee_Schedule.as (26 ln, pure fns) + BB_AutoTest_Employee_ScheduleWindows.as, same commit |
| Compose AutoTest | utils_<feature>::Add produces the right entity shape — children present, fragments present/absent, tags applied | the Utils file | c27dfc88a: BB_Employee_Utils.as + BB_AutoTest_Employee_ComposeFromRoster.as, same commit |
| End-to-end routed AutoTest | the feature reached THROUGH the driver/owner path a player would exercise | the driver-integration commit | 01300d524: wage debit routed via the StoreDriver + BB_AutoTest_Employee_WageDeductsViaStoreDriver.as, same commit |
Not every feature earns all three — a small self-contained feature may collapse to one
compose-plus-behavior test (corpus: Entryway shipped with a single 133-line
DirectionalSignals AutoTest). The norm is: each behavioral claim the commit message makes
has an assertion somewhere in the same commit.
1.2 What is exempt — scope policy, not a laxity license
Pure cosmetic/display glue is the corpus's deliberately untested tail: widgets that only
mirror state, splash text, music selection, signage, interact-prompt display. The policy:
if the code's only failure mode is "looks wrong" — no state mutation, no signal another
system consumes, no economy/inventory effect — a gym or manual PIE check is the verification
tier, and an AutoTest is not required. The moment display glue grows a behavioral edge
(a cooldown, a queue, a purchase side effect), that edge gets an AutoTest like anything else.
Two hard rules regardless of tier:
- Never author tests in Blueprint. BPs are binary — undiffable, unreviewable. Anything a
BP subclass could add is an AS subclass with
default overrides (ck-tests-authoring-and-running
§1, AutoTest spec §11).
- Scenario names state what is VERIFIED, not what the code does:
MoneyFloor,
NoPickupBeforeTime, MultiOccupant — never Test1, Demo (AutoTest spec §4).
2. Layer choice — consumer decision table
Decide by what the assertion needs, not by habit. Mechanics for each row: the cited section
of ck-tests-authoring-and-running.
| Layer | Use when the assertion needs… | Cost | Mechanics |
|---|
| AS AutoTest (PIE) | a ticking ECS world: processors, deferred requests, signals, entity lifecycles. ~95% of feature logic lands here. | one PIE boot amortized across the whole map | §2a |
| Net AS AutoTest (multi-PIE) | client–server: does the value/state actually replicate | multi-world PIE; adding a test requires a C++ rebuild (stub generation — the least discoverable fact in the pipeline, §2b.4); hard 30 s convergence ceiling regardless of _TimeoutSeconds (§2b.7) | §2b |
| Gym | a human's eyes — interactive demo, feel/tuning, manual QA. Built for interactive/visual features; pure-logic features get AutoTests only (corpus: Trashcan has 3 AutoTests, no gym; Shelf has a 10-file gym) | manual PIE session | §2d |
| Gauntlet | a real process: actual boot, real GameMode/input pipeline/navmesh, exit-code contract — player journeys an in-PIE test can't exercise | a fresh editor boot per run — corpus measured ~50–65 s/run, suite ~9–10 min; budget accordingly and don't reach for it when a ticking world suffices | §2e |
| C++ automation test | no world at all — pure math, parsers, formatting, data shapes | milliseconds | §2c |
On the last row: the corpus has zero C++ unit tests in practice — its "pure utility"
code lives in AS namespaces and gets unit-style AutoTests instead (§1.1). If you do write
one, pretty-naming is [UNDER ADJUDICATION — see CkFoundation .claude/reports/ADJUDICATIONS.md A2]: join the feature family's existing prefix; greenfield consumer analogue is
<Game>.<Feature>.*.
3. Project wiring — what a NEW game must set up
Generalized from BusterBlock's wiring (verified 2026-07-03); the bootstrap skill owns the
empty-repo procedure, ck-game-build-and-cook owns the cook/stage half. This section owns
the test-specific wiring decisions.
3.1 A <Game>Tests editor-only plugin — non-negotiable
Host ALL test AngelScript (Script/{Tests,Gyms,Gauntlet,Generated}) plus one C++ module
(for generated net-autotest stubs) in a dedicated plugin:
<Game>Tests.uplugin: "EnabledByDefault": false, depends on CkTests.
- Game
.uproject: enable it with "TargetAllowList": ["Editor"].
Why this is load-bearing — the Shipping-staging incident (BusterBlock e0de34899,
verified): packaged Shipping compiles the project's staged Script/ at boot, but test/gym
scripts inherit CkTests base classes, and CkTests is disabled in Shipping. Result: 431
"unknown super type" errors aborted AS preprocessing and the client never booted. The
first fix was a Script/Dev/ convention; the final home is the editor-only plugin
(68616a8a5 moved 683 files there), which removes test AS from every packaged build by
construction. Pair it with DisablePlugins.Add("CkTests") in Shipping/Test target rules
and DirectoriesToNeverCook for gym/autotest maps — mechanics in ck-game-build-and-cook.
3.2 The AutoTest map config
One AS-defined UCkAutoTestMapConfig asset per test root, pointing the populator at your
AutoTests level. Full setup: AutoTest spec §9 (9a level, 9c config, 9d populator behavior).
The consumer-side discipline: set ClassScanRoot to your plugin's scope explicitly —
the populator auto-saves the map on sync, and an over-broad scan root can wipe or repopulate
the wrong map. Corpus example (BusterBlock):
Plugins/BusterBlockTests/Script/Tests/BB_AutoTestMapConfig.as sets
ClassScanRoot = "/BusterBlockTests/" with a comment saying exactly this.
3.3 Gym registry and Gauntlet dispatcher
- Gyms: a
<Game> gym registry namespace + a game base GameMode that registers project gyms;
templates and console commands in the gym spec §2/§5/§9 (real command set:
Ck_Gym_Restart/Next/Prev/GoTo/List — the spec's ShowInfo/ValidateStations rows are
stale, per Plugins/CkTests/CLAUDE.md's trust table).
- Gauntlet: every project owns a dispatcher mapping scenario names to AS classes —
the dispatcher contract is Gauntlet spec §7. Corpus example (BusterBlock): a repo-root
RunGauntlet.bat registry + RunGauntletAll.ps1 suite/flake harness that parses the .bat
as its single source of truth, keeps expected-failure tests registered but excluded from
the gate, and flags "unexpectedly passing" for status flips. [SINGLE-EXEMPLAR] as a
concrete script pair, but the dispatcher-per-project contract itself is spec-mandated.
Reference files — load only what the task needs
Section numbers cited elsewhere in this skill point into these files.
| Topic | Read |
|---|
| AutoTest patterns and the contamination trap class | references/patterns-and-isolation.md |
6. Evidence standards
Aligned with the framework's evidence rules — ck-tests-authoring-and-running §4 is the
canonical statement (verdict artifacts, stale-binary trap, net-stub rebuild rule, baseline
capture); root doctrine Plugins/CkFoundation/CLAUDE.md non-negotiable #4 (three-environment
verification) and ck-change-control §4 govern anything that touches a public API surface.
Consumer-side deltas worth restating:
6.1 A test run is evidence only if it postdates your last edit AND you read the real verdict
- Re-run after the FINAL edit. A green run against code older than your last
.as save or
C++ build is void — and a failed AS compile means the editor is still running the OLD
compiled code, so the run silently exercised nothing (check the fresh log for
Angelscript: Error lines naming your file before trusting any post-edit run —
ck-tests-authoring-and-running §4).
- Gate on the artifact, not the wrapper: the
-ReportExportPath JSON/HTML, the
LogAutomationController per-test verdict lines, or the process exit code (Gauntlet:
exit code 0 is the contract — Gauntlet spec §9). A toolbox notification or a .bat's
"completed" chatter is a proxy.
- Baseline before "no regressions." Run the target filter before your change; record
pass/fail counts and the NAMES of failing tests; report the delta afterward
("baseline 2 failing {a,b} → still 2 {a,b}"). Corpus expected-failure discipline goes
further: known-red tests stay registered with an expected-failure marker and are excluded
from the gate by name, so an unexpectedly-passing test is itself a reportable event.
6.2 Warnings fail AutoTests — and the sanctioned opt-out is per-test
Harness policy: any Warning- or Error-level log line captured during the test window fails
the test regardless of your FinishSuccess() (AutoTest spec §8 GOTCHA 1). The machinery
(verified 2026-07-03 in Plugins/CkTests/Source/CkTests/Private/CkAutoTestRunner.cpp):
- The runner installs a small default noise list (
GDefaultPlainPatterns,
CkAutoTestRunner.cpp:400-425 — EOS tick chatter, scheduler pump advisories, etc.) as
suppress-all expected errors.
- Everything else needs a per-test
Get_ExpectedLogErrors override
(CkAutoTestRunner.h:82-105; opt-out mechanics: ck-tests-authoring-and-running §2a).
Keep patterns narrow — corpus practice deliberately leaves genuine-saturation lines
("Pump limit [N] reached" breakdowns) failing while suppressing the bounded-burst advisory.
- Never blanket-disable (
_DisableDefaultLogSuppressions exists for the opposite direction —
making a test STRICTER). A warning your test tolerates via a broad pattern is a real
defect you've stopped hearing about. Diagnose first (ck-game-debugging-playbook), suppress
only what you can name.
6.3 Invocation — generic shape only
Editor (Session Frontend / Automation RunTests <filter>), headless
(<Game>Editor-Cmd.exe ... -ExecCmds="Automation RunTests ..." -unattended -nullrhi), and
Gauntlet dispatcher runs: exact command lines, filters, and flags are owned by
ck-tests-authoring-and-running §3 — cite it, don't memorize drifting flag lists. One
consumer-side addition: on multi-session machines, headless boots use -skipcompile so a
killed UBT can't delete module DLLs another session is using (corpus operational rule;
environment detail in ck-build-and-env).
Common mistakes
- Feature commit without its AutoTest. The corpus norm is same-commit (§1); a follow-up
"tests later" commit historically means never.
- Reaching for Gauntlet when a ticking world suffices. ~60 s/boot vs amortized PIE;
Gauntlet is for real-process/input/boot integration only (§2).
- Asserting the same tick as a deferred mutation. Requests and tag/fragment Adds are
deferred — settle or
WaitOneFrame first (§4.1; ckecs-architecture-contract §3).
- Negative assertion without a settle window. "Did not fire" is only meaningful after
the pipeline drained (§4.1).
- Replicated entity script spawned under the autotest entity. Trips the replication
ensure; use the ActorRelay channel (§4.4). An older
_NonReplicating-subclass workaround
exists in the corpus — do not extend it; it bypasses the production replication path.
- Empty
DetectionFilter on test probes. Match-all + shared world = counting other
tests' orphans (§5.1).
FinishSuccess() while your own transients are still landing. They fail the next
test, not yours (§5.2).
- Broad
Get_ExpectedLogErrors patterns that mute real defects along with the noise
(§6.2).
- Claiming green from a run that predates your last edit, or from a wrapper's "done"
signal instead of the verdict artifact (§6.1).
- Blueprint test assets. Never (§1.2).
Provenance and maintenance
Authored 2026-07-03 against BusterBlock superproject 52a75e13d (corpus) and the CkTests
framework docs dated 2026-07-02. Grep/Glob tools are blind under Script/ dirs (repo
.ignore) — all corpus searches below MUST use rg --no-ignore (Git Bash).
Re-verify volatile claims:
git show --stat 01300d524 9a6bfb897 0ddfada98 c27dfc88a
git show -s e0de34899 3688dc72b 1ecf1a5cb de3099e1c
git show --stat 68616a8a5 | tail -3
rg --no-ignore --files Plugins/BusterBlockTests/Script/Tests -g "*.as" | wc -l
rg --no-ignore -l "_TimeoutSeconds" Plugins/BusterBlockTests/Script/Tests -g "*.as" | wc -l
rg --no-ignore -l "ScheduleSettle" Plugins/BusterBlockTests/Script/Tests -g "*.as" | wc -l
rg --no-ignore -l "Request_AcquireChannel" Plugins/BusterBlockTests/Script/Tests -g "*.as" | wc -l
rg --no-ignore -l "Debug_Force" Plugins/BusterBlockTests/Script/Tests -g "*.as" | wc -l
rg -n "GDefaultPlainPatterns|Get_ExpectedLogErrors" Plugins/CkTests/Source/CkTests -g "*.h" -g "*.cpp"
Cited documents: ck-tests-authoring-and-running (CkTests skill — §1 layer table, §2a–2e
authoring, §3 running, §4 evidence rules); Plugins/CkTests/Script/Common/CkAutoTest_CreationSpecification.txt
(§4 naming, §6 base API, §7 patterns, §8 gotchas, §9 per-project setup, §11 scaling);
CkGym_CreationSpecification.txt (§2/§5 templates — check Plugins/CkTests/CLAUDE.md's
trust table for its stale rows); CkGauntlet_CreationSpecification.txt (§7 dispatcher,
§9 exit codes, §10 gotchas); Plugins/CkFoundation/CLAUDE.md (non-negotiable #4);
ck-change-control §4; ckecs-architecture-contract §3. Adjudication watch: A2 (C++ test
naming). If BusterBlockTests moves again or the harness suppression list changes, update
§3.1/§6.2 and the counts above.