The beams were summoned by hand with TEMPSUMMON_MANUAL_DESPAWN and no duration, which InitStats turns into TEMPSUMMON_DEAD_DESPAWN, and NPC 33050 is a trigger with NullCreatureAI that can neither die nor despawn itself. The only thing that removed them was an event on the elder, and his event map stops running the moment he dies, so a kill taken while a wave was up left the beams standing forever. With a 20s summon cycle and a 15s despawn that was most kills. Move the lifetime onto the beam: 33050 gets SmartAI rows that cast Unstable Sun Beam and Photosynthesis when summoned, then cast Unstable Energy and despawn after 18-25s. AIName and ScriptName resolve from the base entry, so the 25-man template is covered too. The react-passive row is required because InitializeReactState gives a SmartAI trigger REACT_AGGRESSIVE; Freya's own Sun Beam (33170) carries the same row. The elder now casts 62207 rather than placing two creatures himself, so the placement comes from the spell: one beam at his feet, plus a forced 62221 on every player in range each summoning one under themselves. That relies on the EffectForceCast fix in #27621. Wave timing from a sniff: first wave 6s after the pull and then every 22-26s, against a fixed 8s and 20s. Beam lifetime 18-25s across eight observed beams, against a fixed 15s. The beam owning its own despawn and the 62207 cast mirror TrinityCore 60388f39073ca85f9592b1e67e88e507a1a79421. The timers come from the sniff and the beam side is SmartAI rather than a C++ AI. Covered by TestUlduar_BrightleafSunBeamsDespawnAfterDeath, which asserts a beam lands on the player and that none outlive the elder. It enters at Brightleaf's own spawn rather than the Freya pad: that pad is 12y from Freya, and aggroing her banishes every living elder, after which he schedules no beams at all. Reported as https://github.com/chromiecraft/chromiecraft/issues/10163 Co-authored-by: Lopin <[email protected]>
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 the existing suite live in the harness:
| Doc | Audience |
|---|---|
| LLM_GUIDE.md | Compact MUST/NEVER + APIs |
| EXAMPLES.md | Full recipes and skeletons |
.agents/docs/e2e-policy.md |
Do not add e2e unless asked; suite conventions |
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). Warden.Enabled = 0on the worldserver, until the harness bug below is fixed upstream. Symptom: every login fails withattempted to log in using invalid client OS ().WorldSocket::HandleAuthSessionreadsaccount.osfrom the database and, when Warden is active, rejects anything that is notWinorOSX. 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 setAC_WARDEN_ENABLED=0on 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:
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 (local/)
e2e/local/ is gitignored except local/README.md. Do not add tests there unless asked. If something is already there:
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 | 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 |
| 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, and one lands under the player rather than all on the elder | P2 | covered (TestAC_27095_*, TestAC_27602_*, TestUlduar_BrightleafSunBeams*); Kologarn Charge blocked-harness (bridge Z after Charge) |
#26266 #27095 #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.
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
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/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:.