# AzerothCore live-stack e2e Protocol-level regression tests for this repository. They import [`github.com/azerothcore/AzerothGhost/e2e/e2eharness`](https://github.com/azerothcore/AzerothGhost) and run against a **live** authserver + worldserver + MySQL. Offline `go test ./...` (without `-tags=e2e`) skips these packages. Authoring rules for the existing suite live in the harness: | Doc | Audience | |-----|----------| | [LLM_GUIDE.md](https://github.com/azerothcore/AzerothGhost/blob/v1.0.8/e2e/LLM_GUIDE.md) | Compact MUST/NEVER + APIs | | [EXAMPLES.md](https://github.com/azerothcore/AzerothGhost/blob/v1.0.8/e2e/EXAMPLES.md) | Full recipes and skeletons | | `.agents/docs/e2e-policy.md` | Do not add e2e unless asked; suite conventions | --- ## Prerequisites 1. Running AzerothCore **3.3.5a** authserver + worldserver. 2. MySQL with `acore_auth`, `acore_characters`, and `acore_world` (world DB is required for spawn cleanup and many fixtures). 3. Go **1.26+** and network reachability to auth (default `127.0.0.1:3724`). 4. `Warden.Enabled = 0` on the worldserver, until the harness bug below is fixed upstream. Symptom: every login fails with `attempted to log in using invalid client OS ()`. `WorldSocket::HandleAuthSession` reads `account.os` from the database and, when Warden is active, rejects anything that is not `Win` or `OSX`. That column is written by the *authserver* from the AUTH_LOGON_CHALLENGE FourCC, and AzerothGhost sends it leading-NUL first (`client/auth.go`: `os := [4]byte{0, 'n', 'i', 'W'}`), so AC's `_os = os.data()` builds an empty string and stores it. Docker stacks can set `AC_WARDEN_ENABLED=0` on the worldserver service. 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: ```bash 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`](./.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): ```bash 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 ```bash 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: ```bash 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 ```bash 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//…` | Hierarchical scenarios (directory = category) | | `suites//` | Suites by domain; issue guards named `TestAC__…` 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 (`local/`) `e2e/local/` is gitignored except `local/README.md`. Do not add tests there unless asked. If something is already there: ```bash make e2e-local # or: go test -tags=e2e ./local/... -count=1 -v -timeout 30m -parallel 1 ``` ### 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; paladin same-aura per-caster + Aura Mastery | P1 | covered (`TestAC_26130_*`, `TestAC_25765_*`) | #26130 #25765 | | spells/cast | Charge on dummy; fail path; stance; Raise Dead + ghoul | P1 | covered (`TestAC_27061_*`) | #27061 | | spells/effects | Charge / grounding totem / Sweeping Strikes Execute; forced cast summons at the forced caster, not at the unit that forced it | P1 | covered (`TestAC_26997_*`, `TestEffects_ForceCastDestination`); dummy-summon `blocked-harness` (engineering dummy lifetime) | #26774 #26997 #27621 | | social/group | form / leave / leader / loot method / disband | P2 | covered | — | | social/loot | need/greed / master loot; below-half kill; a roll that ends on the timer names the roll in its terminating packet, and the member who never voted gets one PASS | P1 | covered (`TestAC_26862_*`, `TestAC_27299_*`); chest mid-roll `blocked-harness` (GO 194821 UseGameObject); pass-on-loot delete `blocked-harness` (item-survive after ALL_PASSED) | #26894 #26862 #27299 #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 | | quests/frostmourne | scrying-orb vision runs; Muradin leaves the cavern and despawns; quest 12478 COMPLETE | P2 | covered (`TestAC_25760_*`); dialogue order and duplicate line `blocked-harness` (no monster-say capture) | #25760 | | quests/objectives | a mob that drops a quest item advertises it, so the client shows the objective on hover (`creature_questitem` -> `SMSG_CREATURE_QUERY_RESPONSE.questItems`) | P2 | covered (`TestAC_27553_*`), decoding the response through a raw packet hook since the harness has no dispatch case for it | #27553 | | quests/summons | using the Serpent Statue on Ranazjar Isle summons Lord Kragaru, the only source of the Book of the Ancients (quest 6027). Beam and summon are asserted separately so a failure names which script broke, and the activation is repeated because the regression it guards was probabilistic, not absolute | P2 | covered (`TestQuest6027_*`) | — | | 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/classic/stratholme | Timmy remains hidden while a relevant Square Scarlet lives, then emerges after the area is clear | P2 | covered (`TestAC_26363_TimmyEmergesAfterSquareCleared`) | #26363 | | instances/ulduar | named tele; Freya wave interval; a Laughing Skull's Lunatic Gaze stops at the brain room's geometry instead of draining sanity through it; Elder Brightleaf's Unstable Sun Beams despawn on their own timer instead of outliving him, one lands under the player rather than all on the elder, and a wave stays capped however many players stand in range; Psychosis and Malady of the Mind stop picking a player once they are at 40 Sanity or less; Algalon holds still for 3s once his Big Bang cast lands instead of resuming melee and Quantum Strike on the next tick | P2 | covered (`TestAC_27095_*`, `TestAC_27602_*`, `TestAC_27590_*`, `TestAC_27539_*`, `TestUlduar_BrightleafSunBeams*`); Kologarn Charge `blocked-harness` (bridge Z after Charge) | #26266 #27095 #27539 #27590 #27602 | | world/gameevents | Call to Arms banners at the Dalaran portals belong to the side they stand on, and the already-correct Warsong set is unchanged. **Wants an exclusive realm**: starting a holiday re-anchors its schedule in the running worldserver until restart; holidays already running are left alone | P2 | covered (`TestAC_24380_*`); Shattrath's 23 positions `gap` | #24380 | --- ## 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. ```go 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: ```go bot.CleanupOwnedSummons(t) ``` ### SoftPass (fail-closed) `SoftPass` / `SoftPassf` **fail the test by default** so unjudgeable fixtures cannot greenwash CI. Only local debug: ```bash E2E_ALLOW_SOFT_PASS=1 go test -tags=e2e … ``` Prefer `Preconditionf` (setup blocked), `Assertf` / `ConfirmedBugf` (oracle), or `HarnessFailf` (infra). --- ## Writing a test (short) ```go //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: 1. `//go:build e2e` + MySQL blank import + `e2eharness`. 2. `meta.Begin` (tag filters; **serial by default** — tag `parallel` only if pad-safe). 3. Unique short `Prefix` (**≤ 7 chars**: account = Prefix + 2 digits + 8 hex, auth max 17); place with **`PackagePad`**. 4. Flow: fixture → place → setup → `CombatReady` if pull → drive → assert. 5. Waiters (**Arm → Send → Wait**), not fixed long sleeps. 6. Severity helpers for fatals; quest DB only after `Save`. 7. 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 Do not add e2e tests unless asked. Do not mention missing coverage unless the user asked about it. `.agents/docs/e2e-policy.md` is suite convention when changing existing tests. This README is **how to run and structure** the suite. --- ## CI Details live in the workflow files only: [`.github/workflows/e2e-live.yml`](../.github/workflows/e2e-live.yml), [`.github/workflows/core-build-nopch.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 should use a **local** stack and the committed suites, not CI setup docs. Greppable failure prefixes: `precondition:`, `AC#N CONFIRMED BUG:`, `harness:`, `WARNING:`.