11 KiB
E2E policy
Mandatory when writing or changing live-stack e2e (e2e/). When to write tests. How: AzerothGhost LLM_GUIDE.md / EXAMPLES.md (this file does not replace them). Inventory: e2e/README.md. Stack: AC 3.3.5a auth+world+MySQL; E2E_* may point at a gateway.
Terms
| Term | Meaning |
|---|---|
| Unit | Isolated C++/Go; no live stack. src/test/. |
| Integration | In-process multi-component; no client login. |
| E2E | //go:build e2e Go test: e2eharness bots on a running realm; assert protocol / object cache / DB. |
| Player-visible | A real client can observe it. |
| Tracked issue | AC issue/PR id for ConfirmedBugf. |
When
First match wins:
| If | Then |
|---|---|
| Pure calc / parse / bit math / no world | UNIT. NEVER e2e. |
Covered by existing unit/mocks (SpellProc*, …) without login/map/DBC fidelity |
UNIT or in-process integration. E2E only if unit cannot reach the path. |
| SQL-only static (text, displayid, non-script loot) | No e2e. Manual optional. |
| SmartAI / conditions / waypoints; outcome only in-world | E2E SHOULD if player-visible and deterministic; else document manual repro. |
| Multi-system on a live session, or protocol/client-observed, or player-repro bugfix | E2E MUST (or update existing). Unit MAY still cover pure pieces. |
| Refactor, no behaviour intent | Update tests that break. Add e2e only if a critical system below lacks an oracle. |
Bias: player-visible and MVT fits → prefer e2e over unit-only. Not a MUST trigger and unsure → do not invent e2e; file a gap.
MUST add or update e2e when any of: player-visible bugfix (encode the repro); protocol change; multi-system (combat/aura/death/quest/loot/mount/pet/vehicle/instance/PvP/guild/relog/DB after .save); crash/hang/freeze on client action (ProbeWorldAlive / AssertWorldAlive); existing TestAC_* path touched (update, do not drop); critical system with no oracle; claimed blizzlike fix for a tracked issue (ConfirmedBugf until fixed).
MUST NOT treat “tested in-game” as a substitute when MVT is feasible.
Harness cannot express it: extend harness or file a harness gap; inventory the gap; still unit-test pure logic.
NEVER / do instead
| Situation | Do instead |
|---|---|
| Pure math, flags, calendar | src/test/ |
| Isolated spell-proc already unit-covered; no new player-visible interaction | extend unit |
| SQL-only static, no behavioural branch | SQL review |
| Rename / comments / refactor; no behaviour; not a critical system | existing tests |
| Human judgment, nondeterminism, hours-long lockouts | manual; optional one deterministic slice |
| Same oracle as an existing test | extend or share a helper |
| “World boots” with no behaviour oracle | not feature e2e |
| Invent harness APIs / test the harness | contribute to AzerothGhost |
SHOULD NOT e2e a GM command that is not on the player path (GM setup is fine; assert player-relevant state).
Authoring (PR gate)
Consumer suite (e2e/suites/ / e2e/smoke/), not only Ghost. //go:build e2e. Import e2eharness + blank MySQL driver.
Fixture: NewSolo / NewScenario (BotSpec / ByRole if roles differ). Prefer ScenarioBot over raw Session except guild charter/bank.
meta.Begin serial by default. Prefix ≤7 chars (auth name max 17 = Prefix+10). Tag parallel only when pad-safe.
Name: TestArea_Behaviour or TestAC_<issue>_<Short>. Comment the issue/PR URL.
Flow: fixture → place → setup (GM ok) → CombatReady if pull → drive → assert.
Place: Teleport / TeleportPad / TeleNamed / GoCreatureID. Melee: GoCreatureID after named tele. Tele clears object cache — re-WaitUnit.
Combat: CombatReady / CombatReadyFull before pulls. NEVER .gm on during aggro. Damage: Damage / DamageKill only — NEVER .gm on mid-fight for .damage.
Casts: Cast / CastMust / CastOrGM / CastAtPosition; fail reason via SpellFailReasonName.
Waiters: Arm → Send → Wait. NEVER re-arm during Wait. NEVER replace waiters with long fixed sleeps.
After fire-and-forget GM that later casts depend on (.pvp on, .gm off), FlushWorld on that bot (world-thread ack) or a state waiter (WaitUnitPvP).
Quest DB: only after Save / QuestStatusAfterSave. Spell-summon: NEVER .npc add instead of the spell. Set race so GM text uses the character’s language (Horde ≠ Common).
Oracle = behaviour (protocol / cache; DB after Save), not “no error”. SHOULD t.Logf("PASS …"). SHOULD log GUIDs/spell ids/statuses.
MVT (all required or it is not coverage): one primary oracle; real client path; setup failures are Preconditionf; correct severity; no footguns above; minutes not hours (long boss waves only if the oracle needs them); runnable with E2E_* on stock AC; name+comment findable from issue or mechanic.
//go:build e2e
// Issue: https://github.com/azerothcore/azerothcore-wotlk/issues/N
func TestAC_N_ShortOracle(t *testing.T) {
meta.Begin(t, meta.TestMeta{Tags: []string{"short", "issue"}, Runtime: "short", Issue: N})
bot := e2eharness.NewSolo(t, e2eharness.ScenarioOpts{
Prefix: "Short", Race: e2eharness.RaceHuman,
Class: e2eharness.ClassWarrior, Level: 80, LearnAllClass: true,
})
// place → setup → (CombatReady) → drive → assert
}
Run: go test -tags=e2e -run TestName -count=1 -v green on fixed core (or only CONFIRMED BUG on unfixed). SHOULD -count=2 stable. Document if exclusive realm needed.
NEVER: invent harness APIs; wrap GM/map without logs; bare t.Fatalf("CONFIRMED BUG"); copy whole examples; one test for five issues.
Fail helpers
Use these, not bare t.Fatalf, for classified outcomes:
| Helper | Prefix | When |
|---|---|---|
Preconditionf |
precondition: |
Setup never reached a judgeable state |
ConfirmedBugf(t, N, …) |
AC#N CONFIRMED BUG: |
Tracked issue; core wrong (fails CI) |
HarnessFailf / Assertf |
harness: / assert: |
Infra or fixed-core regression (fails CI) |
SoftWarnf |
WARNING: |
Non-fatal soft deviation |
| Open / unfixed issue | — | Comment out the entire test: TODO(e2e): re-enable when AC#N is fixed + issue URL. Re-enable body MUST hard-fail. NEVER log+return soft-pass. |
Critical systems
A PR that implements, fixes, or refactors a row MUST add or update e2e for that oracle, or keep an existing test that still asserts it. If harness cannot: gap in e2e/README.md + unit for pure logic.
| System | Oracle / drive |
|---|---|
| Aura apply/strip/consume | apply → action → AssertAuraRemains / AssertAuraConsumed / aura waiters. Paths: Spells, aura scripts, spell_*.cpp |
| Cast / charge / pathing | Cast/CastMust + fail reason / SMSG_SPELL_GO; charge stays on bridge; ground AoE landing |
| Death / repop / corpse | DieAndRepop (or staged) + surviving system (STAY_ALIVE, Raise Dead) |
| Quest + persistence | AddQuest → action → Save → AssertQuestStatus |
| Relog / extra_flags / GM vis | set → Save → Relog → DB or protocol |
| Threat / evade / engage | CombatReady → Engage → still in combat; no evade from bad GM |
| Spell summon | cast the summon spell (not .npc add) + summoned unit properties |
| Instance boss AI | place → CombatReady → Engage → spawn/target/interval (AssertIntervalNotAccelerated) |
| Guild charter / bank | Session guild helpers |
| Account / realm GM | .account set gmlevel scoped to target realm (#27088 style) |
| Crash-prone cast | repro + ProbeWorldAlive / AssertWorldAlive |
| Pure helper extracted from the above | unit the helper and keep e2e on the player path |
Coverage
Search consumer suite + Ghost examples/ + TestAC_* for issue, spell, quest, creature, mechanic before adding.
Same oracle → same test (extend / table-drive). Same setup, different oracle → helper or t.Run, not a second login. NEVER copy Ghost examples/ without changing the oracle. One tracked issue → one primary TestAC_<id>_…; extra edges as t.Run or siblings only if isolation requires. Same root cause + same assertion surface → merge.
Inventory (e2e/README.md + suite comments): category, one-sentence oracle, P0–P3, covered (test name) / gap / blocked-harness / manual-only, issue links, note. MUST update when adding coverage or finding a gap. blocked-harness MUST name the missing API (opcode, waiter, multi-realm, …). Closing a P0/P1 gap is preferred when touching that subsystem. When pruning, merge redundant tests and refresh the inventory.
Prefer one solid test per merged player-visible bugfix. Deepen critical categories over new weak ones. Every test: unique oracle + owner (issue or feature). Delete or merge tests that no longer map to a behaviour.
Priority (next test)
Highest feasible MVT:
| Pri | Cover |
|---|---|
| P0 | Crash / hang / corruption on client action; already-regressed player bug with issue id |
| P1 | Aura strip/consume/persist; death+quest; relog flags; evade/engage; charge/position; totem/grounding; spell-summon (not GM spawn) |
| P2 | Single-boss timers/waves/targeting; guild charter/bank; multi-bot PvP; realm-scoped GM / persisted visibility |
| P3 | QoL, display, nondeterministic farm; full raid clears (out of default growth) |
On a PR: implement e2e for the highest-priority trigger the PR activates. Do not add P3 in the same change unless asked.
Isolation, comments, flakes
Unique Prefix; cleanup via t.Cleanup. Do not depend on other tests’ characters/guilds/instance saves. Time oracles: bounded windows with documented thresholds, not “sleep 60s”.
Tracked issues: URL in comment + id in ConfirmedBugf. State expected green-on-fixed vs CONFIRMED-BUG-on-unfixed. Do not claim blizzlike without issue, PR, or reviewed wowhead/web evidence.
Flake: classify (infra, timing, pad collision, dirty world, core race). Fix waiters / Arm-Send-Wait / CombatReady / unique prefixes / re-wait after tele. Intermittent fail on a correct core is a harness/test bug until proven otherwise. Quarantine (t.Skip + reason) only for environmental flake, rare and ticketed. NEVER silent retries or endless timeout bumps.
Open issues
| Core | Test |
|---|---|
| Unfixed | Comment out the whole test + TODO + URL. Re-enable MUST hard-fail. |
| Fixed | Uncomment; MUST go green. Keep TestAC_<n>_ and comment. |
| Invalid / cannot repro | Delete the disabled block; no lying TODOs. |
| Env cannot run | Preconditionf or inventory blocked-*; not a soft PASS. |
NEVER let a test PASS while the product oracle is wrong. NEVER multi-retry + soft-exit to hide flakes or open bugs. NEVER delete a green regression because the issue closed.
Update tests in the same PR as the behaviour change. Harness renames: update consumers in the same landing window. Ghost e2e/examples/ are patterns; AC regressions live in the consumer suite.
Scratch MUST be e2e/local/ (gitignored except local/README.md). NEVER commit throwaways. Promote into e2e/suites/ next to related tests in the same PR as the fix. Prefer live e2e over ad-hoc GM when the stack is up.
Local (from e2e/): go test -tags=e2e ./.... Official-repo PR and master CI: full suite after nopch clang-18 (reuses those binaries). Dispatch -f scope=smoke for a smaller run. Touch .github/workflows/e2e-live.yml only when changing CI.