diff --git a/.agents/docs/e2e-policy.md b/.agents/docs/e2e-policy.md index ca4fa1c593..f69d37109e 100644 --- a/.agents/docs/e2e-policy.md +++ b/.agents/docs/e2e-policy.md @@ -1,6 +1,13 @@ # 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. +**Do not add, promote, or invent live-stack e2e tests unless the user explicitly asked.** +Do not write `e2e/local/` scratch tests to validate a fix. Existing tests that fail after a +behaviour change may be updated. Scratch, if any, stays in `e2e/local/` (gitignored) and is +never committed. Do not mention missing coverage unless the user asked about it. + +The rest of this file applies only when the user asked to change `e2e/`, or an existing test +broke. **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 @@ -19,17 +26,15 @@ 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. | +| Covered by existing unit/mocks (`SpellProc*`, …) without login/map/DBC fidelity | UNIT or in-process integration. No new e2e unless asked. | | 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. | +| SmartAI / conditions / waypoints; outcome only in-world | No new e2e unless asked. Document a manual repro if useful. | +| Multi-system on a live session, or protocol/client-observed, or player-repro bugfix | No new e2e unless asked. Unit MAY still cover pure pieces. | +| Refactor, no behaviour intent | Update tests that break. Do not add e2e. | -Bias: player-visible and MVT fits → prefer e2e over unit-only. Not a MUST trigger and unsure → do not invent e2e; file a gap. +Do not invent e2e because a change is player-visible. File a gap only if the user asked for coverage. -**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. +**Update** an existing test when the user asked or that test broke. Do not add a new suite, `TestAC_*`, or `e2e/local/` scratch test unless the user asked. Harness cannot express it: extend harness or file a harness gap; inventory the gap; still unit-test pure logic. @@ -106,7 +111,7 @@ Use these, not bare `t.Fatalf`, for classified outcomes: ## 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. +Do not add e2e for a row just because a PR touches that system. Update an existing test only if it broke or the user asked. If harness cannot express something the user asked for: gap in `e2e/README.md` + unit for pure logic. | System | Oracle / drive | |--------|----------------| @@ -129,11 +134,11 @@ Search consumer suite + Ghost `examples/` + `TestAC_*` for issue, spell, quest, 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__…`; 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. +Inventory (`e2e/README.md` + suite comments): category, one-sentence oracle, P0–P3, `covered` (test name) / `gap` / `blocked-harness` / `manual-only`, issue links, note. Update it when the user asked you to add or remove coverage. `blocked-harness` MUST name the missing API (opcode, waiter, multi-realm, …). 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. +Do not add a test per bugfix. Every existing test: unique oracle + owner (issue or feature). Delete or merge tests that no longer map to a behaviour. -## Priority (next test) +## Priority (only if the user asked for a new test) Highest feasible MVT: @@ -144,7 +149,7 @@ Highest feasible MVT: | **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. +On a PR: do not add e2e unless the user asked. If they did, pick the highest-priority trigger they named. Do not add P3 in the same change unless asked. ## Isolation, comments, flakes @@ -167,6 +172,6 @@ NEVER let a test PASS while the product oracle is wrong. NEVER multi-retry + sof 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. +Scratch MUST be `e2e/local/` (gitignored except `local/README.md`). NEVER commit throwaways. Do not promote scratch into `e2e/suites/` unless the user asked. 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. diff --git a/.github/workflows/e2e-live.yml b/.github/workflows/e2e-live.yml index 0010e7bc92..5ef40bbb6a 100644 --- a/.github/workflows/e2e-live.yml +++ b/.github/workflows/e2e-live.yml @@ -54,7 +54,7 @@ on: required: false default: '1' timeout: - description: 'go test -timeout (smoke default 30m; full should be >=120m)' + description: 'go test -timeout (smoke default 30m; full default 60m; job ceiling 90m)' required: false default: '' parallel: @@ -86,11 +86,16 @@ env: jobs: e2e: name: e2e (native / ${{ inputs.scope || 'full' }}) + # github.repository is the base repo on pull_request, so fork PRs still run here. + # This only skips the job inside a fork's own CI. if: github.repository == 'azerothcore/azerothcore-wotlk' # Artifact path (PR nopch): ubuntu-24.04 to match the uploaded binaries. # Dispatch (no artifact): optional self-hosted via E2E_RUNS_ON. + # Ephemeral runners only: MySQL grants persist accounts, and Stop servers + # pkill env/dist/bin/worldserver and authserver. runs-on: ${{ inputs.artifact_run_id != '' && 'ubuntu-24.04' || vars.E2E_RUNS_ON || 'ubuntu-24.04' }} - timeout-minutes: 480 + # Real ceiling. go test -timeout is per package (full default 60m). + timeout-minutes: 90 steps: - name: Free disk run: | @@ -108,6 +113,17 @@ jobs: go-version-file: e2e/go.mod cache-dependency-path: e2e/go.sum + - name: Check gofmt + working-directory: e2e + run: | + set -euo pipefail + dirty="$(gofmt -l .)" + if [[ -n "${dirty}" ]]; then + echo "::error::gofmt needed" + printf '%s\n' "${dirty}" + exit 1 + fi + # AzerothGhost is a public Go module (e2e/go.mod; repo azerothcore/AzerothGhost). - name: Download e2e modules working-directory: e2e @@ -360,7 +376,7 @@ jobs: || { echo "bad timeout duration: ${E2E_TIMEOUT_INPUT}"; exit 1; } timeout="${E2E_TIMEOUT_INPUT}" elif [[ "${scope}" == "full" ]]; then - timeout="180m" + timeout="60m" else timeout="30m" fi diff --git a/AGENTS.md b/AGENTS.md index fd721f8b5e..c263cec076 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -7,8 +7,7 @@ AzerothCore is a C++ MMORPG server emulator for World of Warcraft 3.3.5a (WotLK) - **Do not configure or build unless explicitly asked.** Builds are slow and rarely needed for code changes. - **Never edit SQL files outside `data/sql/updates/pending_db_*/` unless explicitly requested.** `data/sql/base/`, `data/sql/archive/`, and `data/sql/updates/db_*/` are immutable. - Formatting follows `.editorconfig`: UTF-8, LF, max 120 cols, trailing newline, no trailing whitespace; 4-space indent for C++ (tabs forbidden), 2-space for JSON/YAML/sh/ts/js. -- **Prefer live-stack e2e to debug/validate player-visible behaviour** when a local auth+world+MySQL stack is available (protocol, combat, quests, loot, death, multi-bot). See `e2e/README.md` and AzerothGhost `e2e/LLM_GUIDE.md`. Do not invent e2e for pure unit-sized logic — see `.agents/docs/e2e-policy.md`. -- **Scratch e2e only under `e2e/local/`** (gitignored). Never commit throwaway debug tests. Promote keepers into `e2e/suites/` or `e2e/smoke/`. +- **Do not write or add live-stack e2e tests unless the user explicitly asks.** Do not create `e2e/local/` scratch tests to validate a fix. Existing `e2e/` tests that break after a change may be updated. Do not mention missing coverage unless the user asked about it. - Planning docs go in `.agents/plans//` (gitignored), named `..md` (`PLAN`, `REQUIREMENTS`, `ANALYSIS`, …). - **Credit upstream authors.** Code, a mechanism, or data mirrored from another core (TrinityCore, cMaNGOS, …) is committed with `--author` naming the original commit's author (extra sources as `Co-authored-by`), even when rewritten against AC or confirmed by own sniffs; find them in the upstream file's commit history. @@ -24,7 +23,7 @@ Read the matching doc(s) BEFORE starting the task: - Reviewing a changeset or PR → `.agents/docs/code-review.md` - Self-reviewing, or opening or updating a PR → also `.agents/docs/self-review-rules.md` - Touching a subsystem that has a doc in `.agents/docs/systems/` → read that doc too -- Writing, debugging, or changing live-stack e2e (`e2e/`) → `e2e/README.md`, `.agents/docs/e2e-policy.md`, and AzerothGhost `e2e/LLM_GUIDE.md` (scratch work → `e2e/local/`) +- Changing existing live-stack e2e (`e2e/`), and only when the user asked or an existing test broke → `e2e/README.md` and `.agents/docs/e2e-policy.md` - Capturing a lesson or adding/updating agent docs → `.agents/docs/README.md` ## Repository layout @@ -36,7 +35,7 @@ Read the matching doc(s) BEFORE starting the task: - `src/server/shared/` — code shared by auth and world servers. - `src/server/apps/{authserver,worldserver}/` — entry points (ports 3724 and 8085). - `src/test/` — unit tests + mocks. -- `e2e/` — live-stack Go e2e (AzerothGhost harness); see `e2e/README.md`. Scratch/debug tests: `e2e/local/` (gitignored). +- `e2e/` — live-stack Go e2e (AzerothGhost harness); see `e2e/README.md`. - `data/sql/` — `base/` (historical schema), `updates/db_*/` (merged), `updates/pending_db_*/` (in-flight), `custom/` (gitignored). - `modules/` — external modules (see below). - `apps/` — helper scripts; `apps/codestyle/` holds the lint scripts. diff --git a/e2e/README.md b/e2e/README.md index c50018f5e9..6d94d98e58 100644 --- a/e2e/README.md +++ b/e2e/README.md @@ -6,13 +6,13 @@ 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: +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 (LLMs and humans) | +| [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` | When to add e2e vs unit tests (agent/review policy) | +| `.agents/docs/e2e-policy.md` | Do not add e2e unless asked; suite conventions | --- @@ -127,19 +127,16 @@ go test -tags=e2e ./suites/... -run TestPets_SummonWaitDismiss -count=1 -v -time Every live test uses `//go:build e2e` and should call `meta.Begin(t, meta.TestMeta{…})` before expensive setup. -### Scratch / agent debug (`local/`) +### Scratch (`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**. +`e2e/local/` is gitignored except `local/README.md`. Do not add tests there unless asked. If something is already there: ```bash -# 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 | @@ -349,9 +346,11 @@ A test that fails intermittently on a **correct** core is a test/harness bug unt --- -## Policy (when to add e2e) +## Policy -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. +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. --- @@ -367,6 +366,6 @@ Details live in the workflow files only: | 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. +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:`. diff --git a/e2e/local/README.md b/e2e/local/README.md index 3664820abd..44e0dfd184 100644 --- a/e2e/local/README.md +++ b/e2e/local/README.md @@ -1,15 +1,7 @@ # e2e/local — scratch / debug tests (not committed) -Put **throwaway** live-stack tests here while debugging a core/script change. Everything under this directory is **gitignored** except this README. - -## When to use - -| Put it here | Promote to | -|-------------|------------| -| One-off repro while fixing a bug | `suites/…` or `suites/issues/` once it should stay green | -| Agent exploratory scenario | Same — only keep if policy says e2e is warranted | -| Experiments with harness APIs | Drop or move when done | +Do not add tests here unless asked. Do not promote scratch into `smoke/` or `suites/` unless asked. Do **not** put permanent regressions here; CI and reviewers only look at `smoke/` and `suites/`. @@ -21,43 +13,8 @@ From `e2e/` with stack up and `E2E_*` set (see parent [README.md](../README.md)) # all scratch packages go test -tags=e2e ./local/... -count=1 -v -timeout 30m -parallel 1 -# one file’s package (create local/foo/foo_e2e_test.go) +# one existing package go test -tags=e2e ./local/foo -count=1 -v -timeout 15m -parallel 1 ``` From the repository root: `make -C e2e e2e-local`. From `e2e/`: `make e2e-local`. - -## Minimal skeleton - -```go -//go:build e2e - -package local_debug_test - -import ( - "testing" - - _ "github.com/go-sql-driver/mysql" - - "github.com/azerothcore/azerothcore-wotlk/e2e/internal/meta" - "github.com/azerothcore/AzerothGhost/e2e/e2eharness" -) - -func TestLocal_Scratch(t *testing.T) { - meta.Begin(t, meta.TestMeta{ - Tags: []string{"local", "short"}, - Runtime: "short", - Category: "local", - }) - bot := e2eharness.NewSolo(t, e2eharness.ScenarioOpts{ - Prefix: "Loc", - Class: e2eharness.ClassWarrior, - Level: 80, - }) - bot.TeleportPad(t, e2eharness.PackagePad(t)) - // … drive and assert … - t.Logf("PASS local scratch") -} -``` - -Authoring rules: AzerothGhost `e2e/LLM_GUIDE.md`. When to keep e2e long-term: `.agents/docs/e2e-policy.md`. diff --git a/e2e/smoke/smoke_e2e_test.go b/e2e/smoke/smoke_e2e_test.go index 013d07580e..6c1cf91e46 100644 --- a/e2e/smoke/smoke_e2e_test.go +++ b/e2e/smoke/smoke_e2e_test.go @@ -8,8 +8,8 @@ import ( _ "github.com/go-sql-driver/mysql" - "github.com/azerothcore/azerothcore-wotlk/e2e/internal/meta" "github.com/azerothcore/AzerothGhost/e2e/e2eharness" + "github.com/azerothcore/azerothcore-wotlk/e2e/internal/meta" ) // SMOKE-07 first: fresh-account create (run early — late runs saw flaky auth EOF diff --git a/e2e/suites/combat/charm/charm_e2e_test.go b/e2e/suites/combat/charm/charm_e2e_test.go index 933db110b5..3aa59d9592 100644 --- a/e2e/suites/combat/charm/charm_e2e_test.go +++ b/e2e/suites/combat/charm/charm_e2e_test.go @@ -8,8 +8,8 @@ import ( _ "github.com/go-sql-driver/mysql" - "github.com/azerothcore/azerothcore-wotlk/e2e/internal/meta" "github.com/azerothcore/AzerothGhost/e2e/e2eharness" + "github.com/azerothcore/azerothcore-wotlk/e2e/internal/meta" ) // Battle Shout ranks (player-cast, cancelable). Do not use GM .aura / 45614 Blending In diff --git a/e2e/suites/combat/death/death_e2e_test.go b/e2e/suites/combat/death/death_e2e_test.go index 57822d3835..5a5c155480 100644 --- a/e2e/suites/combat/death/death_e2e_test.go +++ b/e2e/suites/combat/death/death_e2e_test.go @@ -8,9 +8,9 @@ import ( _ "github.com/go-sql-driver/mysql" - "github.com/azerothcore/azerothcore-wotlk/e2e/internal/meta" "github.com/azerothcore/AzerothGhost/client" "github.com/azerothcore/AzerothGhost/e2e/e2eharness" + "github.com/azerothcore/azerothcore-wotlk/e2e/internal/meta" ) const ( diff --git a/e2e/suites/combat/pets/pets_e2e_test.go b/e2e/suites/combat/pets/pets_e2e_test.go index 0266df2602..bf3bd7f051 100644 --- a/e2e/suites/combat/pets/pets_e2e_test.go +++ b/e2e/suites/combat/pets/pets_e2e_test.go @@ -8,8 +8,8 @@ import ( _ "github.com/go-sql-driver/mysql" - "github.com/azerothcore/azerothcore-wotlk/e2e/internal/meta" "github.com/azerothcore/AzerothGhost/e2e/e2eharness" + "github.com/azerothcore/azerothcore-wotlk/e2e/internal/meta" ) const spellSummonImp = 688 diff --git a/e2e/suites/combat/threat/threat_e2e_test.go b/e2e/suites/combat/threat/threat_e2e_test.go index 7d369b6d5d..c1cef258fe 100644 --- a/e2e/suites/combat/threat/threat_e2e_test.go +++ b/e2e/suites/combat/threat/threat_e2e_test.go @@ -8,8 +8,8 @@ import ( _ "github.com/go-sql-driver/mysql" - "github.com/azerothcore/azerothcore-wotlk/e2e/internal/meta" "github.com/azerothcore/AzerothGhost/e2e/e2eharness" + "github.com/azerothcore/azerothcore-wotlk/e2e/internal/meta" ) // pullDummy is a high-HP training dummy for kill/engage-without-target oracles. diff --git a/e2e/suites/combat/vehicles/vehicles_e2e_test.go b/e2e/suites/combat/vehicles/vehicles_e2e_test.go index 9eb52da483..61fe0ed50f 100644 --- a/e2e/suites/combat/vehicles/vehicles_e2e_test.go +++ b/e2e/suites/combat/vehicles/vehicles_e2e_test.go @@ -8,32 +8,56 @@ import ( _ "github.com/go-sql-driver/mysql" - "github.com/azerothcore/azerothcore-wotlk/e2e/internal/meta" "github.com/azerothcore/AzerothGhost/e2e/e2eharness" + "github.com/azerothcore/azerothcore-wotlk/e2e/internal/meta" ) -// VEH-* : EnterVehicle / ExitVehicle / IsOnVehicle via Stormwind Steed on PackagePad. +// VEH-* : EnterVehicle / ExitVehicle / IsOnVehicle via Stormwind Steed. +// Armistice (64373) is required by creature_template vehicle conditions. +// spell_area only keeps that aura on the Argent Tournament grounds; PackagePad +// (Outland) applies it then strips it, and VehicleAI ejects the rider. -// spellArmistice (64373) is required by creature_template vehicle conditions -// on Stormwind Steed 33217. VehicleAI/SmartAI ExitVehicle the rider without it -// (conditions.comment: "Vehicle Stormwind Steed requires aura Armistice"). const spellArmistice uint32 = 64373 -func spawnAndBoardSteed(t *testing.T, bot *e2eharness.ScenarioBot) (vehGUID, boarded uint64) { +func placeAtArgentTournament(t *testing.T, bots ...*e2eharness.ScenarioBot) { t.Helper() - bot.ApplyAura(t, spellArmistice) - if !bot.HasAura(spellArmistice) { - e2eharness.Preconditionf(t, "Armistice %d not on player (steed vehicle condition)", spellArmistice) + for i, bot := range bots { + if i == 0 { + bot.TeleNamed(t, "ArgentTournament") + continue + } + x, y, z, m := bots[0].Pos() + bot.Teleport(t, x+1.5*float32(i), y, z, m) } - vehGUID = bot.Spawn(t, e2eharness.CreatureStormwindSteed, 15*time.Second) +} + +func ensureArmistice(t *testing.T, bot *e2eharness.ScenarioBot) { + t.Helper() + // spell_area autocast only. Do not GM-apply: that would hide an area regression. + deadline := time.Now().Add(8 * time.Second) + for time.Now().Before(deadline) { + if bot.HasAura(spellArmistice) { + return + } + time.Sleep(50 * time.Millisecond) + } + e2eharness.Preconditionf(t, "Armistice %d missing after Argent Tournament enter (spell_area)", spellArmistice) +} + +func spawnAndBoardSteed(t *testing.T, bot *e2eharness.ScenarioBot) uint64 { + t.Helper() + ensureArmistice(t, bot) + vehGUID := bot.Spawn(t, e2eharness.CreatureStormwindSteed, 15*time.Second) if vehGUID == 0 { e2eharness.Preconditionf(t, "Stormwind Steed spawn returned guid 0") } - boarded = bot.EnterVehicle(t, vehGUID, 12*time.Second) + // Re-check immediately before click: spell_area can strip a stale apply. + ensureArmistice(t, bot) + bot.EnterVehicle(t, vehGUID, 12*time.Second) if !bot.IsOnVehicle() { e2eharness.Preconditionf(t, "not on steed 0x%X after enter (charm=0x%X)", vehGUID, bot.World.PlayerCharmGUID()) } - return vehGUID, boarded + return vehGUID } // VEH-01: two bots at the pad; one boards a Stormwind Steed. @@ -41,9 +65,8 @@ func TestVehicles_MultiBotColocated(t *testing.T) { meta.Begin(t, meta.TestMeta{Tags: []string{"short", "combat", "multi_bot"}, Runtime: "short", Category: "combat/vehicles"}) bots := e2eharness.NewScenario(t, e2eharness.ScenarioOpts{Prefix: "VehCol", Count: 2, Level: 80}) - pad := e2eharness.PackagePad(t) - e2eharness.TeleportAllPad(t, bots, pad) - vehGUID, _ := spawnAndBoardSteed(t, bots[0]) + placeAtArgentTournament(t, bots...) + vehGUID := spawnAndBoardSteed(t, bots[0]) bots[1].AssertWorldAlive(t) t.Logf("PASS vehicle multi-bot colocation boarded=0x%X", vehGUID) } @@ -53,10 +76,10 @@ func TestVehicles_RelogAfterTeleBaseline(t *testing.T) { meta.Begin(t, meta.TestMeta{Tags: []string{"short", "combat", "protocol"}, Runtime: "short", Category: "combat/vehicles"}) bot := e2eharness.NewSolo(t, e2eharness.ScenarioOpts{Prefix: "VehRl", Level: 80}) - bot.TeleportPad(t, e2eharness.PackagePad(t)) + placeAtArgentTournament(t, bot) bot.Save(t) bot.Relog(t) - vehGUID, _ := spawnAndBoardSteed(t, bot) + vehGUID := spawnAndBoardSteed(t, bot) t.Logf("PASS relog then board steed 0x%X", vehGUID) } @@ -66,8 +89,8 @@ func TestVehicles_HardDisconnectWorldAlive(t *testing.T) { probe := e2eharness.NewSolo(t, e2eharness.ScenarioOpts{Prefix: "VehPr", Level: 10}) vic := e2eharness.NewSolo(t, e2eharness.ScenarioOpts{Prefix: "VehVc", Level: 80}) - vic.TeleportPad(t, e2eharness.PackagePad(t)) - vehGUID, _ := spawnAndBoardSteed(t, vic) + placeAtArgentTournament(t, vic) + vehGUID := spawnAndBoardSteed(t, vic) e2eharness.HardDisconnectAndProbe(t, vic, probe, 0) t.Logf("PASS passenger hard-drop world alive steed=0x%X", vehGUID) } @@ -77,11 +100,11 @@ func TestVehicles_EnterExitStormwindSteed(t *testing.T) { meta.Begin(t, meta.TestMeta{Tags: []string{"short", "combat", "serial"}, Runtime: "short", Category: "combat/vehicles"}) bot := e2eharness.NewSolo(t, e2eharness.ScenarioOpts{Prefix: "VehEn", Level: 80}) - bot.TeleportPad(t, e2eharness.PackagePad(t)) + placeAtArgentTournament(t, bot) if bot.IsOnVehicle() { e2eharness.Preconditionf(t, "already on vehicle before enter") } - vehGUID, _ := spawnAndBoardSteed(t, bot) + vehGUID := spawnAndBoardSteed(t, bot) bot.ExitVehicle(t, 12*time.Second) if bot.IsOnVehicle() { e2eharness.Assertf(t, "still IsOnVehicle after ExitVehicle (0x%X)", bot.VehicleGUID()) @@ -89,5 +112,3 @@ func TestVehicles_EnterExitStormwindSteed(t *testing.T) { bot.AssertWorldAlive(t) t.Logf("PASS EnterVehicle/ExitVehicle Stormwind Steed 0x%X", vehGUID) } - - diff --git a/e2e/suites/guild/charter_bank/guild_e2e_test.go b/e2e/suites/guild/charter_bank/guild_e2e_test.go index a6e073ea62..6293127b9b 100644 --- a/e2e/suites/guild/charter_bank/guild_e2e_test.go +++ b/e2e/suites/guild/charter_bank/guild_e2e_test.go @@ -9,8 +9,8 @@ import ( _ "github.com/go-sql-driver/mysql" - "github.com/azerothcore/azerothcore-wotlk/e2e/internal/meta" "github.com/azerothcore/AzerothGhost/e2e/e2eharness" + "github.com/azerothcore/azerothcore-wotlk/e2e/internal/meta" ) // GUILD-01: charter buy + full turn-in via harness CreateGuildViaCharter (SetupGuildLeader). diff --git a/e2e/suites/instances/bind_reset/bind_reset_e2e_test.go b/e2e/suites/instances/bind_reset/bind_reset_e2e_test.go index 51570c3517..06d190b575 100644 --- a/e2e/suites/instances/bind_reset/bind_reset_e2e_test.go +++ b/e2e/suites/instances/bind_reset/bind_reset_e2e_test.go @@ -8,8 +8,8 @@ import ( _ "github.com/go-sql-driver/mysql" - "github.com/azerothcore/azerothcore-wotlk/e2e/internal/meta" "github.com/azerothcore/AzerothGhost/e2e/e2eharness" + "github.com/azerothcore/azerothcore-wotlk/e2e/internal/meta" ) // BIND-* : instance party baselines + ritual summon (3 roles) + #10708. @@ -248,5 +248,3 @@ func TestBind_CharacterInstanceAfterEnter(t *testing.T) { bot.AssertWorldAlive(t) t.Logf("PASS character_instance rows=%d map=%d", n, m) } - - diff --git a/e2e/suites/instances/classic/stratholme/stratholme_e2e_test.go b/e2e/suites/instances/classic/stratholme/stratholme_e2e_test.go index ac56520cbb..47332b64ed 100644 --- a/e2e/suites/instances/classic/stratholme/stratholme_e2e_test.go +++ b/e2e/suites/instances/classic/stratholme/stratholme_e2e_test.go @@ -125,11 +125,24 @@ func TestAC_26363_TimmyEmergesAfterSquareCleared(t *testing.T) { // Center of the 15 relevant Scarlet spawns in Crusaders' Square. The 55-yard // observation radius excludes other Scarlet spawns with the same entries. bot.Teleport(t, 3660, -3180, 127, stratholmeMap) + bot.CombatReady(t) triggers := waitForTimmyActivationSet(t, bot, 20*time.Second) if len(triggers) != 15 { e2eharness.Preconditionf(t, "loaded Timmy activation area has %d relevant Scarlets, want 15", len(triggers)) } - bot.CombatReady(t) + // Timmy's emerge tick can fire before this client's cache has the Square + // pack (same SHA on ephemeral CI flips pass / "already present"). One + // kill+.respawn after the pack is visible resets his SAI. + if leftover := bot.FindUnit(npcTimmyTheCruel, 100); leftover != 0 { + t.Logf("Timmy 0x%X in cache with Square loaded, killing + .respawn", leftover) + bot.DamageKill(t, []uint64{leftover}, 10_000_000, 10*time.Second) + bot.GM(t, ".respawn") + bot.FlushWorld(t) + triggers = waitForTimmyActivationSet(t, bot, 20*time.Second) + if len(triggers) != 15 { + e2eharness.Preconditionf(t, "after reset, activation area has %d relevant Scarlets, want 15", len(triggers)) + } + } if timmy := bot.FindUnit(npcTimmyTheCruel, 100); timmy != 0 { e2eharness.ConfirmedBugf(t, 26363, "Timmy was already present as 0x%X before the Scarlet activation area was cleared", timmy) } diff --git a/e2e/suites/instances/northrend/ulduar/ulduar_e2e_test.go b/e2e/suites/instances/northrend/ulduar/ulduar_e2e_test.go index c76c384fdd..48597240be 100644 --- a/e2e/suites/instances/northrend/ulduar/ulduar_e2e_test.go +++ b/e2e/suites/instances/northrend/ulduar/ulduar_e2e_test.go @@ -8,8 +8,8 @@ import ( _ "github.com/go-sql-driver/mysql" - "github.com/azerothcore/azerothcore-wotlk/e2e/internal/meta" "github.com/azerothcore/AzerothGhost/e2e/e2eharness" + "github.com/azerothcore/azerothcore-wotlk/e2e/internal/meta" ) // OPEN(e2e): re-enable when AC#26266 is fixed — Charge near Kologarn must not drop below bridge Z. @@ -75,6 +75,7 @@ func TestAC_27095_FreyaAlliesSpawnRateReduction(t *testing.T) { Prefix: "Freya", Level: 80, }) + // Stay GM through the raid enter (.go xyz onto 603 is ignored after .gm off). const ( npcFreya10 = uint32(32906) @@ -100,16 +101,39 @@ func TestAC_27095_FreyaAlliesSpawnRateReduction(t *testing.T) { return "Unknown" } - bot.TeleNamed(t, "Freya") + // Raid interior pad (game_tele BossFreya). .go xyz is reliable; a missing + // custom "Freya" name hangs TeleNamed for 60s. + bot.Teleport(t, 2326.82, -48.131, 424.963, e2eharness.MapUlduar) + if _, _, _, m := bot.Pos(); m != e2eharness.MapUlduar { + e2eharness.Preconditionf(t, "not in Ulduar after Freya pad tele map=%d", m) + } bot.GoCreatureID(t, npcFreya10) + // Now drop GM. FlushWorld/.gps beside Freya can evade her out of cache, + // so re-acquire a living GUID before Engage/FaceUnit. bot.CombatReady(t) - freyaGUID := bot.WaitUnitAny(t, 30*time.Second, npcFreya10, npcFreya25) + // Evade can leave a 0 HP object in cache. Re-poll until we have a living + // Freya, not only when the GUID disappears. + var freyaGUID uint64 + deadline := time.Now().Add(30 * time.Second) + for { + freyaGUID = bot.WaitUnitAny(t, 10*time.Second, npcFreya10, npcFreya25) + if hp, maxHP := bot.UnitHP(freyaGUID); maxHP > 0 && hp > 0 { + if bot.World.GetObject(freyaGUID) != nil { + break + } + } + if !time.Now().Before(deadline) { + e2eharness.Preconditionf(t, "no living Freya in cache after GoCreatureID (last=0x%X)", freyaGUID) + } + time.Sleep(50 * time.Millisecond) + } bot.Engage(t, freyaGUID, 15*time.Second) tr := e2eharness.NewSpawnSetTracker(allyEntries, 3*time.Second) tr.KindOf = func(entry uint32) string { return label(entry) } - sets := tr.WaitSets(t, bot.World, 2, 4*time.Minute) + // Waves are ~60s apart. 90s covers two waves; 4m just delayed a miss. + sets := tr.WaitSets(t, bot.World, 2, 90*time.Second) t.Logf("Set1=%s units=%d Set2=%s units=%d gap=%s", sets[0].Kind, len(sets[0].Guids), sets[1].Kind, len(sets[1].Guids), @@ -118,7 +142,7 @@ func TestAC_27095_FreyaAlliesSpawnRateReduction(t *testing.T) { // Detonating Lashers explode on death — if Set1 is Lashers, wait for Set3 // and kill Set2 (still: older set while a newer set is up). if sets[0].Kind == "Lashers" { - sets = tr.WaitSets(t, bot.World, 3, 4*time.Minute) + sets = tr.WaitSets(t, bot.World, 3, 150*time.Second) } var older, newer e2eharness.SpawnSet @@ -131,7 +155,6 @@ func TestAC_27095_FreyaAlliesSpawnRateReduction(t *testing.T) { e2eharness.Preconditionf(t, "cannot find a non-Lasher older set to kill without collateral explosions") } - time.Sleep(2 * time.Second) tr.Poll(bot.World, time.Now()) var olderLive []uint64 @@ -176,9 +199,9 @@ func TestAC_27095_FreyaAlliesSpawnRateReduction(t *testing.T) { for _, s := range bot.UnitsByEntry(120, allyEntries...) { knownAtKill[s.GUID] = struct{}{} } - fresh := bot.WaitNewUnits(t, knownAtKill, allyEntries, 90*time.Second) + fresh := bot.WaitNewUnits(t, knownAtKill, allyEntries, 75*time.Second) if len(fresh) == 0 { - e2eharness.Preconditionf(t, "no new ally set spawned within 90s after older-set kill") + e2eharness.Preconditionf(t, "no new ally set spawned within 75s after older-set kill") } nextT := time.Now() fromNewer := nextT.Sub(newer.SpawnT) @@ -245,7 +268,7 @@ func TestUlduar_MultiBotLoginNearBossPad(t *testing.T) { Level: 80, }) for _, b := range bots { - b.TeleNamed(t, "Freya") + b.Teleport(t, 2326.82, -48.131, 424.963, e2eharness.MapUlduar) } _, _, _, m0 := bots[0].Pos() _, _, _, m1 := bots[1].Pos() diff --git a/e2e/suites/items/equip/equip_e2e_test.go b/e2e/suites/items/equip/equip_e2e_test.go index 675951817e..29bf20d50c 100644 --- a/e2e/suites/items/equip/equip_e2e_test.go +++ b/e2e/suites/items/equip/equip_e2e_test.go @@ -8,8 +8,8 @@ import ( _ "github.com/go-sql-driver/mysql" - "github.com/azerothcore/azerothcore-wotlk/e2e/internal/meta" "github.com/azerothcore/AzerothGhost/e2e/e2eharness" + "github.com/azerothcore/azerothcore-wotlk/e2e/internal/meta" ) // EQUIP-01: add+inventory oracle (EquipEntry is EQUIP-04). diff --git a/e2e/suites/protocol/session/session_e2e_test.go b/e2e/suites/protocol/session/session_e2e_test.go index 14fa44ce24..d6b1e64fcd 100644 --- a/e2e/suites/protocol/session/session_e2e_test.go +++ b/e2e/suites/protocol/session/session_e2e_test.go @@ -7,8 +7,8 @@ import ( _ "github.com/go-sql-driver/mysql" - "github.com/azerothcore/azerothcore-wotlk/e2e/internal/meta" "github.com/azerothcore/AzerothGhost/e2e/e2eharness" + "github.com/azerothcore/azerothcore-wotlk/e2e/internal/meta" ) // SESS-01: position readable after login (seed of inventory/pos load). diff --git a/e2e/suites/protocol/teleport/teleport_e2e_test.go b/e2e/suites/protocol/teleport/teleport_e2e_test.go index d5d2a1a1f8..76026628b0 100644 --- a/e2e/suites/protocol/teleport/teleport_e2e_test.go +++ b/e2e/suites/protocol/teleport/teleport_e2e_test.go @@ -9,8 +9,8 @@ import ( _ "github.com/go-sql-driver/mysql" - "github.com/azerothcore/azerothcore-wotlk/e2e/internal/meta" "github.com/azerothcore/AzerothGhost/e2e/e2eharness" + "github.com/azerothcore/azerothcore-wotlk/e2e/internal/meta" ) // TELE-01: cross-map teleport (EK → Northrend). diff --git a/e2e/suites/quests/escort/escort_e2e_test.go b/e2e/suites/quests/escort/escort_e2e_test.go index 75dde5d025..883d3f7e1a 100644 --- a/e2e/suites/quests/escort/escort_e2e_test.go +++ b/e2e/suites/quests/escort/escort_e2e_test.go @@ -9,8 +9,8 @@ import ( _ "github.com/go-sql-driver/mysql" - "github.com/azerothcore/azerothcore-wotlk/e2e/internal/meta" "github.com/azerothcore/AzerothGhost/e2e/e2eharness" + "github.com/azerothcore/azerothcore-wotlk/e2e/internal/meta" ) // OPEN(e2e): replace this package with a real escort quest fixture (start → follow → diff --git a/e2e/suites/quests/lifecycle/lifecycle_e2e_test.go b/e2e/suites/quests/lifecycle/lifecycle_e2e_test.go index 07a675dc1c..260133ad6a 100644 --- a/e2e/suites/quests/lifecycle/lifecycle_e2e_test.go +++ b/e2e/suites/quests/lifecycle/lifecycle_e2e_test.go @@ -8,8 +8,8 @@ import ( _ "github.com/go-sql-driver/mysql" - "github.com/azerothcore/azerothcore-wotlk/e2e/internal/meta" "github.com/azerothcore/AzerothGhost/e2e/e2eharness" + "github.com/azerothcore/azerothcore-wotlk/e2e/internal/meta" ) // QLIFE-03 / #26549: STAY_ALIVE quest fails on death. diff --git a/e2e/suites/social/group/group_e2e_test.go b/e2e/suites/social/group/group_e2e_test.go index 5f87b21224..fdff4f2513 100644 --- a/e2e/suites/social/group/group_e2e_test.go +++ b/e2e/suites/social/group/group_e2e_test.go @@ -8,9 +8,9 @@ import ( _ "github.com/go-sql-driver/mysql" - "github.com/azerothcore/azerothcore-wotlk/e2e/internal/meta" "github.com/azerothcore/AzerothGhost/client" "github.com/azerothcore/AzerothGhost/e2e/e2eharness" + "github.com/azerothcore/azerothcore-wotlk/e2e/internal/meta" ) // GRP-01: FormParty → both InGroup, leader flag. diff --git a/e2e/suites/social/loot/loot_e2e_test.go b/e2e/suites/social/loot/loot_e2e_test.go index 29daa726f6..7fb060d750 100644 --- a/e2e/suites/social/loot/loot_e2e_test.go +++ b/e2e/suites/social/loot/loot_e2e_test.go @@ -8,9 +8,9 @@ import ( _ "github.com/go-sql-driver/mysql" - "github.com/azerothcore/azerothcore-wotlk/e2e/internal/meta" "github.com/azerothcore/AzerothGhost/client" "github.com/azerothcore/AzerothGhost/e2e/e2eharness" + "github.com/azerothcore/azerothcore-wotlk/e2e/internal/meta" ) // LOOT-01: Need vs Greed on a guaranteed Uncommon drop (Crimson Templar). diff --git a/e2e/suites/social/trade/trade_e2e_test.go b/e2e/suites/social/trade/trade_e2e_test.go index 72c164eedf..05fb1da930 100644 --- a/e2e/suites/social/trade/trade_e2e_test.go +++ b/e2e/suites/social/trade/trade_e2e_test.go @@ -9,9 +9,9 @@ import ( _ "github.com/go-sql-driver/mysql" - "github.com/azerothcore/azerothcore-wotlk/e2e/internal/meta" "github.com/azerothcore/AzerothGhost/client" "github.com/azerothcore/AzerothGhost/e2e/e2eharness" + "github.com/azerothcore/azerothcore-wotlk/e2e/internal/meta" ) // Cheap stackable trade bait (Linen Cloth). @@ -108,6 +108,15 @@ func waitInvAtLeast(t *testing.T, bot *e2eharness.ScenarioBot, entry uint32, lea return n } +func waitInvEqual(t *testing.T, bot *e2eharness.ScenarioBot, entry uint32, want int, timeout time.Duration) int { + t.Helper() + n := waitInv(t, bot, entry, func(got int) bool { return got == want }, timeout) + if n != want { + e2eharness.Assertf(t, "inventory entry=%d count=%d want=%d", entry, n, want) + } + return n +} + // Spec 1 — Item + gold dual-accept. func TestTrade_ItemGoldDualAcceptInventories(t *testing.T) { meta.Begin(t, meta.TestMeta{Tags: []string{"short", "trade", "multi_bot"}, Runtime: "short", Category: "social/trade"}) @@ -233,24 +242,19 @@ func TestTrade_StackableMerge(t *testing.T) { // After accept: totals conserved and B gains. bag, slot := a.AddItemWait(t, itemLinenCloth, 5) b.AddItemWait(t, itemLinenCloth, 3) - a0 := a.InventoryCount(t, itemLinenCloth) - b0 := b.InventoryCount(t, itemLinenCloth) - if a0 < 5 || b0 < 3 { - e2eharness.Preconditionf(t, "seed failed a=%d b=%d", a0, b0) - } + // Same oracle as the dual-accept test: client push can land before CharDB. + a0 := waitInvAtLeast(t, a, itemLinenCloth, 5, 10*time.Second) + b0 := waitInvAtLeast(t, b, itemLinenCloth, 3, 10*time.Second) e2eharness.OpenTrade(t, a, b) a.SetTradeItem(t, 0, bag, slot) e2eharness.CompleteTrade(t, a, b) - a1 := a.InventoryCount(t, itemLinenCloth) - b1 := b.InventoryCount(t, itemLinenCloth) + a1 := waitInvEqual(t, a, itemLinenCloth, a0-5, 10*time.Second) + b1 := waitInvEqual(t, b, itemLinenCloth, b0+5, 10*time.Second) if a1+b1 != a0+b0 { e2eharness.Assertf(t, "linen not conserved %d+%d → %d+%d", a0, b0, a1, b1) } - if b1 <= b0 { - e2eharness.Assertf(t, "B did not gain stack %d→%d", b0, b1) - } t.Logf("PASS stackable merge/conserve a %d→%d b %d→%d", a0, a1, b0, b1) } diff --git a/e2e/suites/spells/aura/aura_e2e_test.go b/e2e/suites/spells/aura/aura_e2e_test.go index d8c8d84783..7bf8f9be3d 100644 --- a/e2e/suites/spells/aura/aura_e2e_test.go +++ b/e2e/suites/spells/aura/aura_e2e_test.go @@ -8,8 +8,8 @@ import ( _ "github.com/go-sql-driver/mysql" - "github.com/azerothcore/azerothcore-wotlk/e2e/internal/meta" "github.com/azerothcore/AzerothGhost/e2e/e2eharness" + "github.com/azerothcore/azerothcore-wotlk/e2e/internal/meta" ) // Issue: https://github.com/azerothcore/azerothcore-wotlk/issues/26130 diff --git a/e2e/suites/spells/cast/cast_e2e_test.go b/e2e/suites/spells/cast/cast_e2e_test.go index 7e79a4b20d..ab9bff27fe 100644 --- a/e2e/suites/spells/cast/cast_e2e_test.go +++ b/e2e/suites/spells/cast/cast_e2e_test.go @@ -8,8 +8,8 @@ import ( _ "github.com/go-sql-driver/mysql" - "github.com/azerothcore/azerothcore-wotlk/e2e/internal/meta" "github.com/azerothcore/AzerothGhost/e2e/e2eharness" + "github.com/azerothcore/azerothcore-wotlk/e2e/internal/meta" ) // CAST-01: live cast pipeline — Charge succeeds on spawned dummy after CombatReady. diff --git a/e2e/suites/spells/effects/effects_e2e_test.go b/e2e/suites/spells/effects/effects_e2e_test.go index 5de3c9a901..e65b181630 100644 --- a/e2e/suites/spells/effects/effects_e2e_test.go +++ b/e2e/suites/spells/effects/effects_e2e_test.go @@ -8,8 +8,8 @@ import ( _ "github.com/go-sql-driver/mysql" - "github.com/azerothcore/azerothcore-wotlk/e2e/internal/meta" "github.com/azerothcore/AzerothGhost/e2e/e2eharness" + "github.com/azerothcore/azerothcore-wotlk/e2e/internal/meta" ) // OPEN(e2e): re-enable when AC#26774 is fixed