| .. | ||
| internal | ||
| local | ||
| smoke | ||
| suites | ||
| .env.example | ||
| go.mod | ||
| go.sum | ||
| go.work.example | ||
| Makefile | ||
| README.md | ||
AzerothCore live-stack e2e
Protocol-level regression tests for this repository. They import
github.com/azerothcore/AzerothGhost/e2e/e2eharness
and run against a live authserver + worldserver + MySQL.
Offline go test ./... (without -tags=e2e) skips these packages.
Authoring rules for new tests live in the harness:
| Doc | Audience |
|---|---|
| LLM_GUIDE.md | Compact MUST/NEVER + APIs (LLMs and humans) |
| EXAMPLES.md | Full recipes and skeletons |
.agents/docs/e2e-policy.md |
When to add e2e vs unit tests (agent/review policy) |
Prerequisites
- Running AzerothCore 3.3.5a authserver + worldserver.
- MySQL with
acore_auth,acore_characters, andacore_world(world DB is required for spawn cleanup and many fixtures). - Go 1.26+ and network reachability to auth (default
127.0.0.1:3724).
Accounts are created by the harness (GM level 3, password test). Do not reuse real player accounts.
Local harness (optional)
For co-development against a local AzerothGhost checkout:
cd e2e
cp go.work.example go.work # gitignored; edit the replace path
# replace github.com/azerothcore/AzerothGhost => /path/to/AzerothGhost
e2e/go.mod pins github.com/azerothcore/AzerothGhost v1.0.8 (see go.sum).
go test / go mod download fetch that module.
Environment
Copy and adjust e2e/.env.example. Stock AC and CI use acore:acore.
| Variable | Default (harness) | Meaning |
|---|---|---|
E2E_AUTH_ADDR |
127.0.0.1:3724 |
Auth / realm-list address clients use |
E2E_AUTH_DSN |
acore:acore@tcp(127.0.0.1:3306)/acore_auth |
Auth DB |
E2E_CHAR_DSN |
acore:acore@tcp(127.0.0.1:3306)/acore_characters |
Characters DB |
E2E_WORLD_DSN |
acore:acore@tcp(127.0.0.1:3306)/acore_world |
World DB (spawns, tele names, cleanup) |
Optional filters (internal/meta):
| Variable | Effect |
|---|---|
E2E_TAGS |
Comma list; test must include all listed tags (AND) |
E2E_SKIP_TAGS |
Skip if test has any listed tag |
E2E_ISSUE |
Run only tests with matching TestMeta.Issue |
E2E_RUNTIME |
Run only short / med / long |
E2E_ALLOW_SOFT_PASS |
1 only for local debug. SoftPass is fail-closed by default |
Export vars in your shell (or set -a; source .env; set +a) before go test.
How to run
Recommended full suite (pad-safe defaults: serial tests and packages):
cd e2e
# export E2E_* from .env.example first
go test -tags=e2e ./... -count=1 -v -timeout 120m -parallel 1 -p 1
# or:
make e2e-full
-parallel 1 keeps in-package tests serial (meta.Begin is serial by default). -p 1 runs one package at a time so IsolationPads never hash-share. Raising either without a unique preferred pad per concurrent package causes thrash.
Make targets
make e2e-smoke # smoke/ + suites tagged smoke
make e2e-category C=quests # one category tree
make e2e-sub C=spells/aura # one leaf suite
make e2e-issue N=26549 # TestAC_26549_* anywhere
make e2e-tags TAGS_FILTER=smoke,short
make e2e-full # ./... long timeout
make e2e-list # list tests
Override parallel / timeout:
make e2e-full PARALLEL=1 P=1 TIMEOUT=120m
# P = go test -p (packages). Keep 1 unless every concurrent suite has a unique preferred pad.
Single test / package
go test -tags=e2e ./suites/combat/pets -count=1 -v -timeout 30m -parallel 1
go test -tags=e2e ./suites/... -run TestPets_SummonWaitDismiss -count=1 -v -timeout 15m
Layout
| Path | Role |
|---|---|
smoke/ |
Fast login / tele / relog guards |
suites/<category>/… |
Hierarchical scenarios (directory = category) |
suites/<category>/ |
Suites by domain; issue guards named TestAC_<n>_… live beside related tests |
local/ |
Scratch/debug only — gitignored (except local/README.md); never commit throwaways |
internal/meta |
TestMeta + env tag filters + Begin (Parallel policy) |
internal/fixtures |
Re-exports pads / PackagePad for suites that prefer fixtures |
Every live test uses //go:build e2e and should call meta.Begin(t, meta.TestMeta{…}) before expensive setup.
Scratch / agent debug (local/)
When validating a fix on a live stack (player-visible combat, protocol, quests, multi-bot), prefer writing a small e2e under e2e/local/ instead of ad-hoc GM spam or long manual checklists. That tree is not committed.
# create e.g. local/repro/repro_e2e_test.go (//go:build e2e)
make e2e-local
# or:
go test -tags=e2e ./local/... -count=1 -v -timeout 30m -parallel 1
If the scenario should stay as a regression, move it into suites/ next to related tests with proper meta.Begin tags — see .agents/docs/e2e-policy.md.
Inventory
| Category | Oracle | Pri | Coverage | Issue |
|---|---|---|---|---|
| smoke | login / pad tele / relog / world alive | P0 | covered (TestSmoke_*) |
— |
| combat/charm | apply/cancel aura | P1 | covered; Yogg MC logout blocked-harness (no charm/MC drive) |
#25506 |
| combat/death | die → ghost → release → reclaim | P1 | covered | — |
| combat/pets | summon / GUID / attack / dismiss | P1 | covered; dungeon Raise Dead blocked-harness (ready-check / instance summon) |
#27081 |
| combat/threat | engage / taunt switch / kill clears combat | P1 | covered | — |
| combat/vehicles | spellclick steed enter/exit | P2 | covered | — |
| spells/aura | apply/query; CC broken by damage; mount persist | P1 | covered (TestAC_26130_*) |
#26130 |
| spells/cast | Charge on dummy; fail path; stance; Raise Dead + ghoul | P1 | covered (TestAC_27061_*) |
#27061 |
| spells/effects | Charge / grounding totem / Sweeping Strikes Execute | P1 | covered (TestAC_26997_*); dummy-summon blocked-harness (engineering dummy lifetime) |
#26774 #26997 |
| social/group | form / leave / leader / loot method / disband | P2 | covered | — |
| social/loot | need/greed / master loot; below-half kill | P1 | covered (TestAC_26862_*); chest mid-roll blocked-harness (GO 194821 UseGameObject); pass-on-loot delete blocked-harness (item-survive after ALL_PASSED) |
#26894 #26862 #22000 |
| social/trade | item+gold accept; cancel; walk-OOR TARGET_TO_FAR | P1 | covered | #25723 |
| quests/lifecycle | STAY_ALIVE fail on death; status after save/relog | P1 | covered (TestAC_26549_*) |
#26549 |
| quests/escort | find spawned unit; follow-NPC despawns on logout | P2 | covered (TestAC_24450_*) |
#24450 |
| items/equip | visible-item slot after EquipEntry; additem; survives relog | P2 | covered | — |
| protocol/session | pos; item/quest load; money save/relog | P1 | covered; GM vis persist blocked-harness (extra_flags after relog) |
#25793 |
| protocol/teleport | cross-map; named; GoCreatureID | P1 | covered | — |
| guild/charter_bank | charter buy+turn-in | P2 | covered | — |
| instances/bind_reset | party tele; ritual summon | P2 | covered; post-reset summon blocked-harness (AcceptSummon after reset) |
#10708 |
| instances/ulduar | named tele; Freya wave interval | P2 | covered (TestAC_27095_*); Kologarn Charge blocked-harness (bridge Z after Charge) |
#26266 #27095 |
Parallelism and isolation
Model
| Layer | Behaviour |
|---|---|
| Packages | go test -p N runs packages concurrently. Default P=1 in the Makefile — IsolationPads has 27 unique pads; more packages than pads hash-share and can thrash. Raise P only when every concurrent package has a unique preferred pad. |
| Tests in a package | meta.Begin is serial by default (no t.Parallel). Tag parallel only if pad-safe. Makefile PARALLEL=1. |
| Pads | e2eharness.PackagePad(t) is sticky per suite folder for the process lifetime. |
Use PackagePad for combat/social placement — not a shared Stormwind cell for every suite.
pad := e2eharness.PackagePad(t)
bot.TeleportPad(t, pad)
// multi-bot:
e2eharness.FormPartyAtPad(t, pad, leader, mate)
e2eharness.TeleportAllPad(t, bots, pad)
PadStormwindOutskirts is a legacy alias of the AbandonHouse pad. Prefer PackagePad(t).
Isolation pads
Far-apart world locations (operator-captured). Combat-heavy packages have preferred 1:1 assignments; other packages take free pads or a stable hash share when the pool is exhausted.
| Pad name | Map | Notes |
|---|---|---|
Tower1 |
Eastern Kingdoms (0) | Preferred: combat/threat |
Tower2 |
Eastern Kingdoms (0) | Preferred: combat/death |
AbandonHouse |
Eastern Kingdoms (0) | Preferred: combat/pets; legacy SW outskirts alias |
NagrandArena |
Outland (530) | Preferred: combat/charm |
FloatingIsland1 |
Outland (530) | Preferred: combat/vehicles |
FloatingIsland2 |
Outland (530) | Preferred: social/loot |
FloatingIsland3 |
Outland (530) | Preferred: social/group |
InMountains1 |
Kalimdor (1) | Preferred: social/trade |
InMountains2 |
Kalimdor (1) | Preferred: spells/cast |
InMountains3 |
Kalimdor (1) | Preferred: spells/effects |
ElwynnRidge |
Eastern Kingdoms (0) | Elwynn cliff |
BurningSteppes |
Eastern Kingdoms (0) | ~300y from ElwynnRidge |
Mulgore1 |
Kalimdor (1) | Preferred: quests/escort (plains, spawn-safe) |
Mulgore3–Mulgore5, MulgoreNorth |
Kalimdor (1) | Mulgore / Thunder Bluff mesa |
Boulderslide |
Kalimdor (1) | Stonetalon Boulderslide Ravine |
Stonetalon1, Stonetalon2 |
Kalimdor (1) | Stonetalon Mountains |
Talondeep |
Kalimdor (1) | Talondeep Path (near Ashenvale1) |
Ashenvale1 |
Kalimdor (1) | Ashenvale |
FelwoodSouth, MorlosAran |
Kalimdor (1) | Felwood |
HyjalApproach, Hyjal1, Hyjal2 |
Kalimdor (1) | Hyjal |
Unlisted suites (smoke, spells/aura, protocol, quests, items, instances, guild, …) receive the first free pad, then hash-share if needed. Logs include PackagePad suite=… pad=….
Do not hardcode one shared coordinate for every new combat test. Content that must use a fixed world location (tabard designer, instance entrance, named tele) still may — then return to the package pad when possible.
Cleanup rules
Live e2e mutates a real realm. Cleanup is mandatory.
Persistent spawns (.npc add / .gobject add)
These write DB rows. Bare add without cleanup litters pads (e.g. Crimson Templar 15209, Gift of the Observer GO 194821).
| Do | Do not |
|---|---|
bot.Spawn / SpawnKillLootable / SpawnGameObject (register cleanup) |
Bare .npc add / .gobject add |
DespawnCreatureSpawn / DespawnGameObjectSpawn by DB spawn id |
Assume process exit cleans world DB |
| Rely on SQL DELETE + soft live despawn (harness does both) | Only live delete after socket already closed |
Despawn path: SQL DELETE always (survives session close) + optional soft .npc delete / .gobject delete while the socket is open.
Loot: do not use .npc add temp for loot tests (TEMPSUMMON_CORPSE_DESPAWN removes the corpse; use Spawn / SpawnKillLootable).
Pets, ghouls, guardians, totems
NewScenario / NewSolo register CleanupOwnedSummons on t.Cleanup (dismiss pet + despawn units with SUMMONEDBY/CREATEDBY = player).
After Raise Dead / heavy summon use, also call while still InWorld:
bot.CleanupOwnedSummons(t)
SoftPass (fail-closed)
SoftPass / SoftPassf fail the test by default so unjudgeable fixtures cannot greenwash CI. Only local debug:
E2E_ALLOW_SOFT_PASS=1 go test -tags=e2e …
Prefer Preconditionf (setup blocked), Assertf / ConfirmedBugf (oracle), or HarnessFailf (infra).
Writing a test (short)
//go:build e2e
package pets_test
import (
"testing"
_ "github.com/go-sql-driver/mysql"
"github.com/azerothcore/azerothcore-wotlk/e2e/internal/meta"
"github.com/azerothcore/AzerothGhost/e2e/e2eharness"
)
func TestPets_Example(t *testing.T) {
meta.Begin(t, meta.TestMeta{
Tags: []string{"med", "combat", "pets"},
Runtime: "med",
Category: "combat/pets",
})
bot := e2eharness.NewSolo(t, e2eharness.ScenarioOpts{
Prefix: "PetEx",
Class: e2eharness.ClassWarlock,
Level: 80,
LearnAllClass: true,
})
bot.TeleportPad(t, e2eharness.PackagePad(t))
// setup (GM ok) → CombatReady before pulls → drive → assert
t.Logf("PASS …")
}
Checklist:
//go:build e2e+ MySQL blank import +e2eharness.meta.Begin(tag filters; serial by default — tagparallelonly if pad-safe).- Unique short
Prefix(≤ 7 chars: account = Prefix + 2 digits + 8 hex, auth max 17); place withPackagePad. - Flow: fixture → place → setup →
CombatReadyif pull → drive → assert. - Waiters (Arm → Send → Wait), not fixed long sleeps.
- Severity helpers for fatals; quest DB only after
Save. - Spawn helpers with cleanup; summons cleaned via
CleanupOwnedSummons.
Full API surface: harness LLM_GUIDE.md / EXAMPLES.md.
Common flakes and fixes
| Symptom | Likely cause | Fix |
|---|---|---|
| NPC ignores bot / no aggro | GM mode still on | CombatReady / CombatReadyFull before pull |
| Boss resets mid-fight | .gm on for .damage |
Damage / DamageKill (mode off) |
Melee out of range after .tele |
Named tele short of target | GoCreatureID after TeleNamed |
| Unit not in cache after tele | Cache clear on transfer | Re-WaitUnit after tele / WaitInWorld |
| Random combat on pad | Leftover spawns / pets / other package thrash | Cleanup helpers; -parallel 1; PackagePad |
| Cast fails “in combat” on empty pad | Contested pad / leftover temp | CombatStop; despawn litter; unique pad |
| Loot roll never starts | Wrong creature or threshold | CreatureGroupLootFixture (15209) + LootThresholdUncommon |
| Quest status wrong in DB | Asserted before save | Save / QuestStatusAfterSave |
| GM commands silently ignored | Wrong race language | Set Race on bot (harness uses racial language) |
SOFT-PASS disabled fatal |
SoftPass without opt-in | Use real severity helpers, or E2E_ALLOW_SOFT_PASS=1 locally only |
precondition: spam |
Stack/DB/env mismatch | Check E2E_*, world up, DSN DB names |
| Intermittent pass without code change | Sleeps / races | Replace sleeps with waiters; fix Arm→Send→Wait |
A test that fails intermittently on a correct core is a test/harness bug until proven otherwise. Do not hide flakes with longer sleeps or SoftPass.
Severity markers
| Helper | Meaning |
|---|---|
Preconditionf |
Setup never reached a judgeable state |
ConfirmedBugf(t, issue, …) |
Core wrong for tracked AC issue/PR |
HarnessFailf |
Infra: timeout, SQL, empty cache, send error |
Assertf / AssertBugf |
Post-drive product oracle |
SoftWarnf |
Non-fatal note |
SoftPass |
Fail-closed unless E2E_ALLOW_SOFT_PASS=1 |
Policy (when to add e2e)
Use .agents/docs/e2e-policy.md for decision trees (e2e vs unit, mandatory triggers, MVT). This README is how to run and structure the suite; the harness guides are how to author scenarios.
CI
Details live in the workflow files only:
.github/workflows/e2e-live.yml,
.github/workflows/core-build-nopch.yml.
| How | Effect |
|---|---|
| Non-draft PR in azerothcore/azerothcore-wotlk | nopch ubuntu-24.04/clang-18 compiles + dry-run, then full e2e reuses those binaries |
Merge to master |
same clang-18 nopch build, then full e2e again (flake + merge-base drift) |
Actions → e2e-live → Run workflow (official repo; needs workflow on default branch, or gh workflow run … --ref e2e) |
Compiles on the runner; choose scope (smoke/full) |
Day-to-day development and agent debugging should use a local stack + e2e/local/ or the committed suites — not CI setup docs.
Greppable failure prefixes: precondition:, AC#N CONFIRMED BUG:, harness:, WARNING:.