comments: the scripts say what they do and how to run them
Each header is a one-line summary, the environment it reads and its usage; the history, measurements and record numbers go. Only comment lines change: with comments stripped the scripts are byte-identical. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,18 +1,9 @@
|
||||
#!/usr/bin/env bash
|
||||
# PreToolUse(Edit|Write|NotebookEdit) guard: refuse to write anything into the 1.12.1 install.
|
||||
# Exit 2 blocks the call and hands stderr back to the agent.
|
||||
#
|
||||
# benilla reads a WoW install and never writes to one (docs/METHOD.md). The client persists
|
||||
# everything through `crate::local_state` into `benilla-config/` beside the binary, and
|
||||
# `scripts/smoke.sh` fails a run that leaves the install changed; this hook is the same rule for
|
||||
# the tools. The install is the player's own copy of somebody else's game, and "what here is
|
||||
# benilla's?" is only answerable while nothing of ours is anywhere else.
|
||||
#
|
||||
# Scoped by physical path, not by name: a checkout's `WoW` may be a symlink, so a write can arrive
|
||||
# spelled either way and only `cd -P` collapses the two. `$WOW_DATA` is honoured because that is
|
||||
# the resolver's own first step.
|
||||
#
|
||||
# `BENILLA_ALLOW_INSTALL_WRITE=1` overrides: a deliberate exception names itself in the transcript.
|
||||
# Reads the hook JSON on stdin; exit 2 blocks the call, with stderr as the reason. The protected
|
||||
# roots are WOW_DATA and the `WoW` link of the primary checkout and of the cwd.
|
||||
# BENILLA_ALLOW_INSTALL_WRITE=1 lets a write through.
|
||||
input=$(cat)
|
||||
|
||||
path=$(printf '%s' "$input" | jq -r '.tool_input.file_path // .tool_input.notebook_path // empty' 2>/dev/null)
|
||||
@@ -28,15 +19,13 @@ esac
|
||||
|
||||
[ -n "${BENILLA_ALLOW_INSTALL_WRITE:-}" ] && exit 0
|
||||
|
||||
# Resolve the target against its nearest EXISTING ancestor — a Write names a file, and may name
|
||||
# directories, that do not exist yet.
|
||||
# Resolve the target through its nearest existing ancestor: a Write may name new directories too.
|
||||
dir=$(dirname "$path")
|
||||
while [ ! -d "$dir" ] && [ "$dir" != "/" ] && [ "$dir" != "." ]; do dir=$(dirname "$dir"); done
|
||||
[ -d "$dir" ] || exit 0
|
||||
real=$(cd -P "$dir" 2>/dev/null && pwd) || exit 0
|
||||
|
||||
# The roots to protect. `$WOW_DATA` first (the resolver's own order), then the `WoW` link every
|
||||
# checkout carries. Both physicalised, so the symlink and its target are one root.
|
||||
# Compare physical paths (`cd -P`): `WoW` may be a symlink, and a write can name either spelling.
|
||||
own=$(git -C "$(dirname "$0")" rev-parse --path-format=absolute --git-common-dir 2>/dev/null)
|
||||
primary=$(cd "${own:-.}/.." 2>/dev/null && pwd)
|
||||
for root in "${WOW_DATA:-}" "$primary/WoW" "$cwd/WoW"; do
|
||||
|
||||
+5
-34
@@ -1,43 +1,14 @@
|
||||
#!/usr/bin/env bash
|
||||
# The addon-API coverage instrument (decisions 1178 §, 1188 phase 0, 1189) — how much of the
|
||||
# 1.12.1 client's global surface benilla presents, measured on any day.
|
||||
# Addon-API coverage: how much of the 1.12.1 client's global surface benilla's UI VM presents.
|
||||
#
|
||||
# scripts/api-coverage.sh # the report
|
||||
# scripts/api-coverage.sh --missing # + every engine global we do not have
|
||||
# scripts/api-coverage.sh --beyond # + every global we have that 1.12 does not
|
||||
#
|
||||
# **Both sides are asked, not remembered.** The 1.12 side is `reference/1.12-globals.tsv`, the
|
||||
# running reference client's own in-world `_G` (regenerate: scripts/gen-reference-globals.py). Our
|
||||
# side is a real `UiScript::new()` dumped through `pairs(_G)`. The previous version of this script
|
||||
# inferred both by pattern-matching source — that is how the arc got "54 C_Container references"
|
||||
# (a grep over Rust, not API surface) and the 124-then-41 corpus estimates, all wrong. When the
|
||||
# question is what a running system exposes, ask the running system.
|
||||
#
|
||||
# It also no longer measures Bagnon/Questie/WeakAuras out of the Era install: 1188 settled the
|
||||
# target as 1.12.1 and the vanilla ecosystem, so an Era addon's call sites are the wrong client's
|
||||
# demand. The real-addon half of the question belongs to the vanilla addon harness (1188 phase 6).
|
||||
#
|
||||
# **The addon corpora are not 1.12 codebases, and that shapes what a grep over them means.**
|
||||
# The vanilla addon corpus (`benilla_formats::addon_corpus`) holds addons that RUN on 1.12 — which is
|
||||
# not the same as addons written for 1.12 and nothing else. Most of the big ones ship one codebase
|
||||
# for several clients and pick a path at load: pfUI registers every module with a version list
|
||||
# (`RegisterModule("loot", "vanilla:tbc", …)`, matched against `pfUI.expansion`), pfQuest keeps a
|
||||
# whole `compat/client.lua` off `GetBuildInfo`, and `libs/` directories carry vendored libraries
|
||||
# with their own client targets. Addons also come in several versions of themselves — a corpus is one snapshot of each.
|
||||
#
|
||||
# So a call site found by grep may be TBC-era code that a 1.12 client reaches and raises on, an
|
||||
# addon calling a name it defines itself under its own namespace, or genuinely 1.12 demand. The
|
||||
# three read very differently and look identical to `grep -rn`. Decision 2146 is the case that
|
||||
# made the point: six 2.0 globals were kept here for years on the strength of call sites that
|
||||
# turned out to be all of the first two kinds.
|
||||
#
|
||||
# **Never quote the percentage undifferentiated** (1178's rule, and 1188 restates it). A missing
|
||||
# global is one of three different things, and only reading the list tells you which:
|
||||
# · a verb for a feature benilla has not built at all → not a gap, a backlog item
|
||||
# · a verb missing from a feature benilla ships → a real hole, fix it
|
||||
# · a name we have that 1.12 does not → a superset, and not free
|
||||
# The last one is the one this script insists on printing in full: an addon that feature-detects
|
||||
# (`if strmatch then`) takes a path we cannot honour, and the failure surfaces far from the cause.
|
||||
# The 1.12 side is `reference/1.12-globals.tsv`, the reference client's in-world `_G`
|
||||
# (scripts/gen-reference-globals.py); ours is a real `UiScript::new()` dumped through `pairs(_G)`.
|
||||
# Never quote the percentage alone: a missing global is an unbuilt feature or a hole in a shipped
|
||||
# one, and a global beyond 1.12 sends a feature-detecting addon down a path we cannot honour.
|
||||
set -u
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
|
||||
+11
-24
@@ -1,28 +1,15 @@
|
||||
#!/usr/bin/env bash
|
||||
# check.sh — the ROUND's verify (decision 1822): fmt everywhere (cheap), clippy + test scoped to
|
||||
# the crates this round's changes can actually affect — the changed crates plus everything that
|
||||
# depends on them. The workspace-wide chain is `scripts/gates.sh`'s job, paid at sync→land.
|
||||
# The in-loop verify: fmt everywhere, clippy and test on the changed crates and their dependents.
|
||||
#
|
||||
# Why this verb exists: the fleet was paying the FULL chain per round — measured over the fortnight
|
||||
# to 2026-09-01 at 3,028 `cargo test --workspace` runs (72 h of wall time) and 1,572 workspace-wide
|
||||
# clippys (33 h) against only 393 actual gate moments, one session hitting the whole suite 161
|
||||
# times in a day while editing a single leaf crate. 59 % of edits land in `benilla-app`, which
|
||||
# nothing but the `benilla` bin depends on: the honest scope of a round is 1–3 crates, not 19.
|
||||
# The change set is the fork point against main plus staged, unstaged and untracked files. A path
|
||||
# under crates/<dir>/ scopes that package, assets/ui/ scopes benilla-app, docs/ and *.md scope
|
||||
# nothing, and any other path runs scripts/gates.sh, the workspace-wide chain run at land.
|
||||
# CHECK_FULL=1 goes straight to gates.sh.
|
||||
#
|
||||
# Scope rule — conservative by construction, unknown means FULL, never "probably fine":
|
||||
# crates/<dir>/** → that crate (owner read off cargo metadata, not the dir name)
|
||||
# assets/ui/** → benilla-app (ui_script's shipped-UI tests read it at runtime)
|
||||
# *.md → no gate can change (gates.sh's docs-only rule)
|
||||
# anything else → escalate: exec scripts/gates.sh (workspace manifests, .cargo/,
|
||||
# rust-toolchain, scripts/, shaders outside crates, …)
|
||||
# Change set = fork point vs main + staged + unstaged + untracked — the round's work, not the last
|
||||
# edit. CHECK_FULL=1 skips straight to gates.sh.
|
||||
#
|
||||
# Same fail-fast discipline as gates.sh: never compose `cargo … | tail` pipelines by hand — the
|
||||
# pipe's exit code masks the gate's (that bug bit twice before gates.sh existed).
|
||||
# scripts/check.sh
|
||||
set -uo pipefail
|
||||
|
||||
# Gate the tree you are STANDING IN (gates.sh learned this; same rule here).
|
||||
# Check the checkout you stand in when it is one of this repo, else this file's (as gates.sh does).
|
||||
here="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
root="$(git rev-parse --show-toplevel 2>/dev/null || true)"
|
||||
[ -n "$root" ] && [ -f "$root/scripts/check.sh" ] || root="$here"
|
||||
@@ -71,8 +58,7 @@ if [ -z "$dirlist" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# ── Changed crates → reverse-dependency closure, off cargo metadata (dir names are not package
|
||||
# names here: crates/mpq is benilla-mpq) ──────────────────────────────────────────────────────
|
||||
# ── Changed crates → reverse-dependency closure, off cargo metadata ──────────────────────────────
|
||||
pkgs="$(python3 - "$root" $dirlist <<'PY'
|
||||
import json, subprocess, sys, os
|
||||
root, dirs = sys.argv[1], set(sys.argv[2:])
|
||||
@@ -112,6 +98,7 @@ echo "check: (changed crates + everything that depends on them; full chain sti
|
||||
log="$(mktemp "${TMPDIR:-/tmp}/benilla-check.XXXXXX")"
|
||||
skips="$(mktemp "${TMPDIR:-/tmp}/benilla-check-skips.XXXXXX")"
|
||||
trap 'rm -f "$log" "$skips"' EXIT
|
||||
# Each gate logs to a file tailed afterwards: piping through `tail` would mask its exit code.
|
||||
run() {
|
||||
local name="$1"; shift
|
||||
if ! "$@" >"$log" 2>&1; then
|
||||
@@ -125,8 +112,8 @@ run() {
|
||||
run fmt cargo fmt --all -- --check
|
||||
run clippy cargo clippy $pflags --all-targets -- -D warnings
|
||||
run test env BENILLA_SKIP_LOG="$skips" cargo test $pflags
|
||||
# The data-gated tests skip without the install or the addon corpus, and libtest swallows the
|
||||
# line; the resolver logs each skip to $BENILLA_SKIP_LOG and this counts them (gates.sh says why).
|
||||
# Data-gated tests skip without the install or the addon corpus and libtest swallows their line,
|
||||
# so each skip is also logged to $BENILLA_SKIP_LOG and counted here.
|
||||
if [ -s "$skips" ]; then
|
||||
echo " test: $(wc -l <"$skips" | tr -d ' ') data-gated tests SKIPPED on this machine —"
|
||||
sort "$skips" | uniq -c | sort -rn | sed 's/^ *\([0-9]*\) \(.*\)/ \1 × \2/'
|
||||
|
||||
+6
-26
@@ -1,26 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
# cine.sh — play one cinematic on the probe and print what happened, as a timeline.
|
||||
#
|
||||
# Every cinematic question is the same shape — "what did the client do to the world while the
|
||||
# camera was away?" — and answering it by hand costs four environment variables, a GM command, a
|
||||
# move trace and a fistful of greps. Assembled by hand three times in one session (decisions
|
||||
# 1701/1707/1708), it produced two WORTHLESS runs before a usable one:
|
||||
#
|
||||
# * the first parked the body wherever the character was saved, which was inside Thunder Bluff's
|
||||
# geometry — the body was wedged, sent nothing, and the trace said "held" for reasons that had
|
||||
# nothing to do with the cinematic;
|
||||
# * the second read "no movement packet" as proof that input was suppressed, when the body was
|
||||
# simply in free fall and had no movement to send.
|
||||
#
|
||||
# Both are avoidable by construction, so this parks the body on known-flat ground first and always
|
||||
# prints the control facts (did it fall? did it move?) beside the thing under test.
|
||||
#
|
||||
# THE SILENT-BUT-LIVE TRICK, which is the other half of why this exists: `$WOW_NOSOUND` opens no
|
||||
# audio device, so `zone::start_music_stream` returns before it logs and a silent run can tell you
|
||||
# NOTHING about music. Instead this points `$BENILLA_HOME` at a throwaway config with
|
||||
# `MasterVolume = "0"` — the device opens, every stream starts and logs its file, and the room
|
||||
# stays quiet. That is what made "the Stormwind city-intro stinger is what plays under the human
|
||||
# narration" a readable fact rather than a guess.
|
||||
# Play one cinematic on the probe and print what happened, as a timeline.
|
||||
#
|
||||
# scripts/cine.sh # the human intro (81), parked in Stormwind
|
||||
# scripts/cine.sh 41 # the dwarf intro
|
||||
@@ -28,8 +7,10 @@
|
||||
# scripts/cine.sh 81 --at "-8913,554,94,0"
|
||||
# scripts/cine.sh 81 --keep # keep the raw log and trace paths, printed at the end
|
||||
#
|
||||
# The probe identity is the checkout's `.probe-identity`, or WOW_USER/WOW_PASS/WOW_CHAR
|
||||
# (scripts/probe-identity.sh).
|
||||
# Logs in as `.probe-identity`, else WOW_USER/WOW_PASS/WOW_CHAR. The body parks on flat ground
|
||||
# first, and did-it-fall/did-it-move print beside the timeline: a wedged or falling body sends no
|
||||
# movement. Sound is live but muted through a BENILLA_HOME config (`MasterVolume = "0"`), because
|
||||
# under WOW_NOSOUND `zone::start_music_stream` returns before it logs.
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
@@ -60,8 +41,7 @@ trace="$work/move.trace"
|
||||
|
||||
IFS=, read -r px py pz pmap <<< "$park"
|
||||
|
||||
# The schedule, in probe-clock seconds. Generous rather than tight: a cold slot streams a city
|
||||
# slowly, and a measurement taken before the world arrived is the wedged-body run all over again.
|
||||
# The schedule, in probe-clock seconds; generous, because a cold start streams a city slowly.
|
||||
park_at=8
|
||||
play_at=20
|
||||
key_at=$((play_at + 8))
|
||||
|
||||
+15
-36
@@ -1,11 +1,9 @@
|
||||
#!/usr/bin/env bash
|
||||
# Generate docs/MAP.md, the "what's built" map, derived entirely from what is on disk so it cannot
|
||||
# drift (docs/METHOD.md: the generated map is regenerated from the code, never hand-written). It is
|
||||
# regenerated and committed on every landing; run it by hand only to look, never edit its output.
|
||||
# Deterministic: identical tree, identical output (no timestamps), so a diff means the structure
|
||||
# actually changed.
|
||||
# No `pipefail`/`-e`: greps that legitimately find nothing (a single-file lib, a crate with no bins)
|
||||
# must not abort the generator.
|
||||
# Generate docs/MAP.md, the map of what is built, from what is on disk.
|
||||
#
|
||||
# Regenerated and committed at every land; run it by hand only to look, never edit the output. No
|
||||
# timestamps, so an identical tree gives an identical map. No `pipefail`/`-e`: a grep that finds
|
||||
# nothing (a single-file lib, a crate with no bins) must not abort the generator.
|
||||
set -u
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
@@ -25,14 +23,8 @@ field() { grep -m1 "^$2" "$1" 2>/dev/null | sed -E "s/^$2[[:space:]]*=[[:space:]
|
||||
for ct in crates/*/Cargo.toml; do
|
||||
name=$(field "$ct" name)
|
||||
desc=$(field "$ct" description)
|
||||
# `bevy.workspace = true` is the form every crate here actually uses, and the old pattern
|
||||
# (`^bevy[[:space:]]*=`) matched only `bevy =` — so workspace-inherited deps read "no Bevy".
|
||||
# benilla-assets shipped mislabelled long enough to say "(no Bevy) — Bevy AssetSource +
|
||||
# AssetLoaders" in one line. The trailing class accepts `.`, whitespace and `=`, which covers
|
||||
# `bevy.workspace =`, `bevy = { … }` and `bevy="…"`, while still rejecting a `bevyfoo` crate.
|
||||
# Load-bearing beyond tidiness: "does this crate need Bevy" is the seam question 0068 cut
|
||||
# `benilla-ui` on and 1160 cuts `benilla-world` on, so a map that lies about it misleads
|
||||
# exactly the work that reads it.
|
||||
# Crates here write `bevy.workspace = true`: the trailing class accepts `.`, whitespace and `=`
|
||||
# (`bevy = { … }`, `bevy="…"` too) and still rejects a `bevyfoo` crate.
|
||||
if grep -qE '^(bevy|bevy_egui|avian3d)[.[:space:]=]' "$ct"; then bevy="Bevy"; else bevy="no Bevy"; fi
|
||||
echo "- **$name** ($bevy) — ${desc:-—}"
|
||||
done
|
||||
@@ -46,10 +38,7 @@ field() { grep -m1 "^$2" "$1" 2>/dev/null | sed -E "s/^$2[[:space:]]*=[[:space:]
|
||||
| sed -E 's/add_plugins\(//; s/([a-z_]+::)+//' | awk '!seen[$0]++ {print "- " $0}'
|
||||
echo
|
||||
|
||||
# The two dev groups are one `add_plugins` line each in `lib.rs` (decisions 1173/1174), so the
|
||||
# list above names the group and not what is in it. Read their members out of `dev.rs` — a map
|
||||
# that stops naming the instruments the moment they move behind the `dev` feature is exactly the
|
||||
# drift this file exists to prevent.
|
||||
# `lib.rs` adds each dev group in one line, so their members are read out of `dev.rs`.
|
||||
echo "### Behind the \`dev\` feature (\`crates/benilla-app/src/dev.rs\` — compiled out by \`--no-default-features\`)"
|
||||
echo
|
||||
grep -oE 'add_plugins\(([a-z_]+::)*[A-Za-z_]+Plugins?' crates/benilla-app/src/dev.rs \
|
||||
@@ -69,8 +58,7 @@ field() { grep -m1 "^$2" "$1" 2>/dev/null | sed -E "s/^$2[[:space:]]*=[[:space:]
|
||||
|
||||
echo "## CLI binaries"
|
||||
echo
|
||||
# Both spellings cargo accepts: `src/bin/<name>.rs` and the directory form `src/bin/<name>/main.rs`
|
||||
# (benilla-extract, benilla-world) — the file-only glob listed three of five for months.
|
||||
# Both forms cargo accepts: `src/bin/<name>.rs` and `src/bin/<name>/main.rs`.
|
||||
for b in crates/*/src/bin/*.rs crates/*/src/bin/*/main.rs; do
|
||||
[ -f "$b" ] || continue
|
||||
crate=$(echo "$b" | sed -E 's@crates/([^/]+)/.*@\1@')
|
||||
@@ -82,8 +70,7 @@ field() { grep -m1 "^$2" "$1" 2>/dev/null | sed -E "s/^$2[[:space:]]*=[[:space:]
|
||||
done | LC_ALL=C sort
|
||||
echo
|
||||
|
||||
# Every shader in the tree is compiled into the binary and addressed by the crate that owns it
|
||||
# (decision 1175), so the crate is part of the shader's name now — list them all, per crate.
|
||||
# Shaders are embedded and addressed by their owning crate, so the crate is part of the name.
|
||||
echo "## WGSL shaders (\`crates/*/src/shaders/\`, embedded)"
|
||||
echo
|
||||
for s in crates/*/src/shaders/*.wgsl; do
|
||||
@@ -97,17 +84,13 @@ field() { grep -m1 "^$2" "$1" 2>/dev/null | sed -E "s/^$2[[:space:]]*=[[:space:]
|
||||
echo
|
||||
echo "> The headless/dev instrument fleet, discovered from the code: every \`WOW_*\` env var"
|
||||
echo "> some \`.rs\` reads, and where it's read — the doc comment at the read site is the"
|
||||
# Don't name individual dev keys here — this line has gone stale twice (it still said 'P perf'
|
||||
# after 0585 moved P onto the chord, and 'backtick panel' after 1043 moved the last two). The
|
||||
# keys live in one place, \`debug_panel::DEV_CHORD\` and the panel footer; name the plane only.
|
||||
# Name no dev keys here: they are listed once, in the debug panel's footer.
|
||||
echo "> semantics. (The in-window surfaces — the Ctrl+Shift dev-chord overlays — are plugins"
|
||||
echo "> above; this indexes the switches that don't announce themselves.)"
|
||||
echo
|
||||
# Any quoted "WOW_*" literal in code — reads go through env::var but also through helpers
|
||||
# (`knob("WOW_FX_AGE", …)`), so match the literal, not the call. Doc comments write the
|
||||
# backticked/`$`-prefixed form, so they don't false-positive.
|
||||
# The probe registry (`capture/probe_env.rs`, 2266 §A5) names every WOW_PROBE* variable as a
|
||||
# quoted literal too; it is the table, not a read site, so it is not a place a switch is used.
|
||||
# Match any quoted "WOW_*" literal: reads also go through helpers (`knob("WOW_FX_AGE", …)`), and
|
||||
# doc comments write the unquoted form. The probe registry `capture/probe_env.rs` is a table,
|
||||
# not a read site, so it is left out.
|
||||
grep -rHoE '"WOW_[A-Z0-9_]+"' crates --include='*.rs' 2>/dev/null \
|
||||
| grep -v '^crates/benilla-app/src/capture/probe_env\.rs:' \
|
||||
| sed -E 's@^crates/@@; s/:"/\t/; s/"$//' \
|
||||
@@ -117,11 +100,7 @@ field() { grep -m1 "^$2" "$1" 2>/dev/null | sed -E "s/^$2[[:space:]]*=[[:space:]
|
||||
END { if (v) print line }'
|
||||
echo
|
||||
|
||||
# Every script, with the first sentence of its own header (2331). The scripts are the
|
||||
# instruments the WOW_* switches are read WITH — a trace reader, a two-client probe, a corpus
|
||||
# census — and a 2026-09-22 sweep found eight of them referenced by nothing but the record that
|
||||
# built them: findable only by someone who remembered the name. This is the index; the header
|
||||
# is the source, so it cannot drift. (`winlab/` is the Windows lab laptop's, 2205/2211.)
|
||||
# Every script, with the first sentence of its own header.
|
||||
echo "## Scripts (\`scripts/\`)"
|
||||
echo
|
||||
for f in scripts/*.py scripts/*.sh; do
|
||||
|
||||
+10
-20
@@ -1,22 +1,14 @@
|
||||
#!/usr/bin/env bash
|
||||
# probe-identity.sh — who a scripted run logs in as. Sourced by smoke.sh, cine.sh and
|
||||
# summon-live.sh; never run on its own.
|
||||
# Who a scripted run logs in as; sourced by smoke.sh, cine.sh and summon-live.sh, never run alone.
|
||||
#
|
||||
# A scripted run has no default account: a login kicks whoever holds the account, so the run
|
||||
# logs in as the account this checkout DECLARES, or as the one the shell names, or not at all.
|
||||
# A login kicks whoever holds the account, so there is no default: the checkout's `.probe-identity`
|
||||
# (tree root, uncommitted; `WOW_USER=…`, `WOW_PASS=…`, `WOW_CHAR=…`, one per line), else the
|
||||
# shell's WOW_USER, WOW_PASS and WOW_CHAR. The client refuses a scripted login on any account but
|
||||
# the declared one (`run_mode::account_guard`).
|
||||
#
|
||||
# `.probe-identity` at the tree root — `WOW_USER=…`, `WOW_PASS=…`, `WOW_CHAR=…`, one per line,
|
||||
# never committed. A machine that runs several checkouts
|
||||
# against one server gives each its own account this way,
|
||||
# and the client refuses a scripted login from that
|
||||
# checkout on any other account (run_mode::account_guard).
|
||||
# WOW_USER, WOW_PASS, WOW_CHAR — the shell's, all three, when nothing is declared.
|
||||
#
|
||||
# probe_identity <tag> <tree root>
|
||||
#
|
||||
# sets PROBE_USER, PROBE_PASS, PROBE_CHAR, PROBE_DECLARED (non-empty when the file supplied them)
|
||||
# and PROBE_HOST with PROBE_AUTH_PORT from WOW_HOST (default localhost:3724). Prints one line
|
||||
# naming the identity; a refusal prints why, with the caller's tag, and returns 1.
|
||||
# `probe_identity <tag> <tree root>` sets PROBE_USER, PROBE_PASS, PROBE_CHAR, PROBE_DECLARED
|
||||
# (non-empty when the file supplied them) and PROBE_HOST, PROBE_AUTH_PORT from WOW_HOST (default
|
||||
# localhost:3724), and prints the identity it chose; a refusal prints why and returns 1.
|
||||
probe_identity() {
|
||||
local tag="$1" root="$2" file="$2/.probe-identity" v ignored=""
|
||||
PROBE_DECLARED=""
|
||||
@@ -55,10 +47,8 @@ probe_identity() {
|
||||
return 0
|
||||
}
|
||||
|
||||
# Is a server listening? `probe_server_or_skip <tag>` prints a skip and returns 1 when the auth
|
||||
# port does not answer — and, for a local server, when the stock world port (8085) does not
|
||||
# either, because a refused world connection reads as a client bug in the log and costs a full
|
||||
# build to discover. A remote server's world port comes from its realm list and is not guessed.
|
||||
# `probe_server_or_skip <tag>` prints a skip and returns 1 when the auth port does not answer, or,
|
||||
# for a local server, the stock world port 8085; a remote world port comes from its realm list.
|
||||
probe_server_or_skip() {
|
||||
local tag="$1" port ports="$PROBE_AUTH_PORT"
|
||||
case "$PROBE_HOST" in localhost | 127.0.0.1) ports="$ports 8085" ;; esac
|
||||
|
||||
+40
-132
@@ -1,28 +1,16 @@
|
||||
#!/usr/bin/env bash
|
||||
# **The fourth gate, as a command.** docs/METHOD.md asks for `fmt` · `clippy` · `test` · **a clean run**,
|
||||
# and until this existed only the first three had a runner. The fourth was a paragraph in
|
||||
# `docs/METHOD.md`, which is a different thing from a verb: a session that does not happen to read that
|
||||
# paragraph concludes it *cannot* run the client, says so to the director, and ships a change whose
|
||||
# whole subject is a live session boundary without ever having crossed one. That happened (2277).
|
||||
# The clean-run gate: boot the real client, log out, re-enter, walk the realm list, check the log.
|
||||
#
|
||||
# What it does: boots the real client against the server (WOW_HOST, default localhost) as the
|
||||
# account this checkout declares — or as the WOW_USER/WOW_PASS/WOW_CHAR of the shell — seats a
|
||||
# body in the world, `/logout`s to character select, **re-enters**, and exits — then reads
|
||||
# the log back. It is the smallest run that crosses every session edge, which is exactly the set of edges
|
||||
# the unit tests can only model.
|
||||
# Needs a server and opens a window for about a minute; gates.sh points at it but does not run it.
|
||||
# Logs in as the checkout's `.probe-identity`, else the shell's WOW_USER/WOW_PASS/WOW_CHAR
|
||||
# (scripts/probe-identity.sh). WOW_HOST is the server (default localhost), WOW_DATA the install
|
||||
# watched for writes (default the tree's WoW link), WOW_BG passes through to the client, and
|
||||
# WOW_SMOKE_KEEP=1 keeps the logs.
|
||||
#
|
||||
# It is deliberately NOT wired into `gates.sh`: it wants a server, opens a window, and costs ~45 s,
|
||||
# so paying it on every commit is the director's call, not this script's. Run it when a change
|
||||
# touches a session boundary — and the gates print a pointer at you either way.
|
||||
#
|
||||
# scripts/smoke.sh # the checkout's .probe-identity
|
||||
# WOW_USER=u WOW_PASS=p WOW_CHAR=Name scripts/smoke.sh # or your test account
|
||||
# WOW_SMOKE_KEEP=1 scripts/smoke.sh # keep the log and print its path
|
||||
# scripts/smoke.sh
|
||||
set -uo pipefail
|
||||
|
||||
# The tree we are standing in, `gates.sh`'s rule verbatim: the git toplevel of $PWD when it is a
|
||||
# checkout of this repo, else this file's — a script reached by absolute path from another
|
||||
# checkout must not run that one.
|
||||
# Test $PWD's checkout when it is one of this repo, else this file's (the same rule as gates.sh).
|
||||
root="$(git -C "$PWD" rev-parse --show-toplevel 2>/dev/null || true)"
|
||||
if [ -z "$root" ] || [ ! -f "$root/scripts/smoke.sh" ]; then
|
||||
root="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
@@ -30,28 +18,8 @@ fi
|
||||
cd "$root" || exit 1
|
||||
echo "smoke: $root"
|
||||
|
||||
# **The smoke declares its whole environment; it does not inherit one.**
|
||||
#
|
||||
# Every leg below is a *run definition* — this account, this character or deliberately none, this
|
||||
# smoke switch — and every one of them was previously composed against whatever `WOW_*` the calling
|
||||
# shell happened to carry. The realm leg is where that stopped being theoretical: it names
|
||||
# `WOW_USER`/`WOW_PASS` and pointedly NOT `WOW_CHAR`, because the walk drives *character select* and
|
||||
# a seated body means there is no roster screen to drive. A session that had exported `WOW_CHAR` for
|
||||
# its last probe — which is how every probe in `docs/METHOD.md` is launched — handed the leg the one
|
||||
# thing it must not have, and the client dutifully entered the world while the walk waited for a
|
||||
# screen that was never coming. The report was `the realm walk did not finish within the timeout`:
|
||||
# 120 s, no cause, and nothing in the log that looks wrong.
|
||||
#
|
||||
# So the scrub is per-VARIABLE and up front, not per-leg: a gate whose result depends on the shell
|
||||
# it was typed in is not a gate. Kept: `WOW_DATA` and `WOW_HOST` (where the install and the server
|
||||
# ARE — locations, not dials), `WOW_BG` (the maintainer's "let me watch this one", which changes
|
||||
# window stacking and nothing else), and `WOW_SMOKE_KEEP` (this script's own). Loud, because a
|
||||
# session that meant to pass something should see it go rather than wonder why it did nothing.
|
||||
#
|
||||
# **Who the run logs in as** is decided first, by `scripts/probe-identity.sh`: the account this
|
||||
# checkout declares in `.probe-identity` (a login kicks whoever holds the account, so a checkout
|
||||
# never logs in as anyone else's), else the WOW_USER/WOW_PASS/WOW_CHAR of the shell — taken before
|
||||
# the scrub, and the one thing the scrub does not throw away.
|
||||
# Resolve the account first, then unset every inherited WOW_* the case below does not keep, so
|
||||
# each leg runs on the switches it names; WOW_BG is kept because it only changes window stacking.
|
||||
. "$root/scripts/probe-identity.sh"
|
||||
probe_identity smoke "$root" || exit 1
|
||||
user="$PROBE_USER"
|
||||
@@ -67,40 +35,26 @@ done
|
||||
[ -n "$inherited" ] &&
|
||||
echo "smoke: ignoring inherited env —$inherited (each leg names its own; a gate is not shell-dependent)"
|
||||
|
||||
# The server, before the client: a refused connection reads as a client bug in the log and costs a
|
||||
# full build to discover. Loud skip, never a silent pass — `gates.sh`'s posture for the install.
|
||||
# Check the server before the build: a refused connection would read as a client bug in the log.
|
||||
probe_server_or_skip smoke || exit 0
|
||||
|
||||
log="$(mktemp "${TMPDIR:-/tmp}/benilla-smoke.XXXXXX")"
|
||||
stamp="$(mktemp "${TMPDIR:-/tmp}/benilla-smoke-stamp.XXXXXX")"
|
||||
before="$(mktemp "${TMPDIR:-/tmp}/benilla-smoke-before.XXXXXX")"
|
||||
# The stamp and the file list always go; the LOG is the one a session may want to keep. Both in one
|
||||
# trap so the early `fail` exits below cannot strand a temp file.
|
||||
# One EXIT trap, so an early `fail` strands no temp file; WOW_SMOKE_KEEP spares the log.
|
||||
trap 'rm -f "$stamp" "$before"; [ -n "${WOW_SMOKE_KEEP:-}" ] || rm -f "$log"' EXIT
|
||||
|
||||
# **The install is READ-ONLY, and this is where that is measured** (decision 1486). benilla reads a
|
||||
# WoW install; it never writes to one. The rule is easy to state and impossible to keep by memory —
|
||||
# one `create_dir_all` under a path derived from `wow_data()` and a player's install has benilla's
|
||||
# litter in it, on a folder that on this machine is SHARED with the sibling RE repo. So the fourth
|
||||
# gate measures it: a full run of the real client, across a logout and a re-login, must leave the
|
||||
# install byte-for-byte as it found it.
|
||||
#
|
||||
# Pure POSIX `find` on purpose — `stat`'s format flag is `-f` on BSD and `-c` on GNU, and a check
|
||||
# that silently no-ops on the other platform is worse than none. The name list catches additions
|
||||
# and deletions; `-newer` against a stamp touched just before the run catches modifications in
|
||||
# place. ~7 ms over the 927-file tree.
|
||||
# The install is read-only: the run must add, remove and modify nothing in it. The sorted file
|
||||
# list catches additions and removals, `-newer` against the stamp catches edits in place; plain
|
||||
# POSIX `find` because `stat`'s format flag differs between BSD (-f) and GNU (-c).
|
||||
install_root=""
|
||||
if [ -n "${WOW_DATA:-}" ]; then
|
||||
install_root="$WOW_DATA"
|
||||
elif [ -d "$root/WoW" ]; then
|
||||
install_root="$root/WoW"
|
||||
fi
|
||||
# Who ELSE might write in there. The install may be shared with the reference client, which the
|
||||
# maintainer runs against the same tree constantly for RE comparison. That client
|
||||
# writes its own WTF (Config.wtf, SavedVariables.lua, the per-character caches) while it runs, so a
|
||||
# whole-tree mtime diff cannot tell "benilla wrote to the install" from "the reference client was
|
||||
# open at the same time". Record it up front so the report below can name the right cause instead of
|
||||
# blaming benilla for someone else's writes.
|
||||
# A running reference client (wow.exe) writes its own WTF files into a shared install, and this
|
||||
# watch cannot tell those from benilla's; note it so the verdict can say so.
|
||||
reference_client_before=""
|
||||
if pgrep -f '[w]ow\.exe' >/dev/null 2>&1; then
|
||||
reference_client_before=1
|
||||
@@ -112,10 +66,7 @@ if [ -n "$install_root" ]; then
|
||||
echo "smoke: NOTE — the reference client (wow.exe) is running; the install watch cannot attribute"
|
||||
fi
|
||||
|
||||
# **Build first, THEN time the run.** The timeout below bounds the *client*, and a cold slot's
|
||||
# build is minutes — folding the two together fails the gate with "the client did not exit", which
|
||||
# names the wrong thing and sends the next session hunting a hang that is a compile. Build errors
|
||||
# still fail here, loudly and as themselves.
|
||||
# Build before the timed run: the timeout bounds the client, and a cold build takes minutes.
|
||||
if ! cargo build -q -p benilla >"$log" 2>&1; then
|
||||
echo "SMOKE FAILED: the client did not build"
|
||||
sed -E 's/\x1b\[[0-9;]*m//g' "$log" | tail -30
|
||||
@@ -123,14 +74,12 @@ if ! cargo build -q -p benilla >"$log" 2>&1; then
|
||||
fi
|
||||
|
||||
echo "smoke: running the logout/re-login round trip (~45 s, opens a window)…"
|
||||
# WOW_NOSOUND: the smoke is an agent's run, and `sound/mod.rs` opens no device under it — without
|
||||
# it every smoke of every session played the login zone's music into the room for 45 s (2006).
|
||||
# WOW_NOSOUND: the client opens no audio device (`sound/mod.rs`), so the run is silent.
|
||||
WOW_UNATTENDED=1 WOW_NOSOUND=1 WOW_USER="$user" WOW_PASS="$pass" WOW_CHAR="$char" WOW_LOGOUT_SMOKE=1 \
|
||||
timeout 180 cargo run -q -p benilla >"$log" 2>&1
|
||||
code=$?
|
||||
|
||||
# Strip ANSI once: the tracing level is a coloured field, and matching `ERROR` against the raw line
|
||||
# also matches `ErrorsFrame.xml` — a false positive that would train everyone to ignore the check.
|
||||
# Strip ANSI once: the tracing level is colour-wrapped, and the checks below match plain text.
|
||||
plain="$(sed -E 's/\x1b\[[0-9;]*m//g' "$log")"
|
||||
|
||||
fail() {
|
||||
@@ -140,8 +89,8 @@ fail() {
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Named before the timeout, because it IS the timeout's usual cause: an unseatable `WOW_CHAR` parks
|
||||
# the client on the roster screen forever, and "did not exit" is the least useful way to say so.
|
||||
# Before the timeout check, which it usually explains: a WOW_CHAR not on the account leaves the
|
||||
# client waiting at the roster.
|
||||
printf '%s' "$plain" | grep -q "not on this account" &&
|
||||
fail "$char is not on $user — the client sat at the roster (make it, or rig it: WOW_RIG=…)"
|
||||
[ $code -eq 124 ] && fail "the client did not exit within the timeout"
|
||||
@@ -150,27 +99,17 @@ printf '%s' "$plain" | grep -q "logout-smoke: empty roster" &&
|
||||
printf '%s' "$plain" | grep -q "logout-smoke: done" ||
|
||||
fail "the round trip never completed (no 'logout-smoke: done')"
|
||||
|
||||
# **The second entry has to be DRIVABLE, not merely reached** (B306, decision 1542). The round trip
|
||||
# crossed this boundary on every run since 2277 and only ever checked that it happened — so the
|
||||
# report that arrived was a character who re-entered the world and could not move at all: vmangos
|
||||
# roots you for the `/logout` countdown, we acked the grant, and the state outlived the session that
|
||||
# was granted it. Nothing here could see that. Now the re-entry leg names whatever would kill WASD
|
||||
# and this refuses anything but `none`.
|
||||
# The re-entered character must be drivable: the leg lists what would block movement (a /logout
|
||||
# root must end with its session), and anything but `none` fails.
|
||||
sup="$(printf '%s\n' "$plain" | sed -n 's/.*logout-smoke: re-entered.*suppressors: \(.*\), done.*/\1/p' | tail -1)"
|
||||
printf ' %-24s %s\n' "re-entry suppressors" "${sup:-<unreported>}"
|
||||
[ "$sup" = "none" ] ||
|
||||
fail "the re-entered character could not be driven — suppressors: ${sup:-<unreported>} \
|
||||
(1542: everything the ended session granted its mover dies with it)"
|
||||
|
||||
# **Did this run actually cross the ROOTED logout?** It did not, and it cannot: the probe accounts
|
||||
# are `gmlevel 6` (decision 0530 — probes need GM for `.go`/`.cheat`) and the deploy sets
|
||||
# `InstantLogout = 1`, so vmangos takes the instant branch of `CMSG_LOGOUT_REQUEST`
|
||||
# (`MiscHandler.cpp`: resting OR taxi OR security >= the config) and returns without ever calling
|
||||
# `SetRooted(true)`. B306's precondition is on the OTHER side of that `return`. So the drivable
|
||||
# check above, for B306 specifically, passes without exercising it — and a check that can pass
|
||||
# vacuously has to say so out loud, or a green run reads as coverage it does not have. That is the
|
||||
# same blind spot as the bug: not a wrong answer, an unasked question. B306's own regression lives
|
||||
# where it can be forced — the unit test at `player::wire_in::session_end_tests`.
|
||||
# vmangos skips the logout root when resting, on a taxi or at account security >= `InstantLogout`
|
||||
# (`MiscHandler.cpp`, `CMSG_LOGOUT_REQUEST`); probe accounts are GM, so report whether the check
|
||||
# above met a real root. `player::wire_in::session_end_tests` forces the rooted case.
|
||||
if printf '%s\n' "$plain" | grep -q "mover mode Root granted"; then
|
||||
printf ' %-24s %s\n' "rooted logout" "yes — the drivable check above is a real pass"
|
||||
else
|
||||
@@ -183,21 +122,9 @@ panics="$(printf '%s\n' "$plain" | grep -cE 'panicked at')"
|
||||
[ "$panics" -ne 0 ] && fail "$panics panic(s)"
|
||||
[ "$errors" -ne 0 ] && fail "$errors ERROR line(s)"
|
||||
|
||||
# **The session invariant** (1290): the UI is built per login, so a two-login run must show every
|
||||
# per-login step exactly twice. One occurrence means the second login re-entered the first login's
|
||||
# frame tree — the "always Onewarrior" failure, which is invisible in a screenshot and silent in
|
||||
# every log unless something counts.
|
||||
#
|
||||
# **Two counts, not one, since 2226.** This walk builds FOUR Lua states, not two: the client sits
|
||||
# at the character screen on a boot VM, and the world entry now BUILDS its own rather than adopting
|
||||
# that one (the reference's `0x490bd0` ↔ `0x48fbf0` pair). So a marker's expected count depends on
|
||||
# which of the two things it tracks — every VM ever built, or only the VMs that loaded the in-game
|
||||
# UI. Conflating them is what this loop used to do, and it was right only for as long as the entry
|
||||
# adopted the glue VM.
|
||||
#
|
||||
# Which makes the first group a 2226 regression detector in its own right: **2 there rather than 4
|
||||
# means a login inherited the character screen's session** — and with it every `VmMemo` that
|
||||
# session had already spent, which is the login one-shot class (1348, B376) straight back.
|
||||
# A marker counts once per edge it fires on. Each world entry builds a world VM beside the
|
||||
# character screen's glue VM (the reference's `0x490bd0`/`0x48fbf0` pair), so per-VM markers count
|
||||
# twice per login; half that means an entry adopted the glue VM and its spent `VmMemo`s.
|
||||
sessions=2 # world entries in this walk
|
||||
vms=$((sessions * 2)) # …and the Lua states they cost: a glue VM and a world VM each (2226)
|
||||
for marker in "Fonts.xml loaded"; do
|
||||
@@ -207,12 +134,8 @@ for marker in "Fonts.xml loaded"; do
|
||||
fail "'$marker' happened $n time(s), expected $vms — one per VM built, two per login (2226); \
|
||||
$sessions would mean the entry adopted the character screen's VM and inherited its spent VmMemos"
|
||||
done
|
||||
# **The keybinding table is an entry-edge seed since 2241**: `seed_bindings_for_vm` runs inside
|
||||
# `load_ingame_ui_on_world_entry`, beside the zone channels and the default language, so its
|
||||
# marker counts once per LOGIN, not once per VM. This loop read it against `$vms` for one day —
|
||||
# 2226 wrote the count on 09-14, 2241 moved the seed on 09-15 — and every smoke after that landing
|
||||
# failed here on a tree whose VM count above was exactly right; found bisecting a render change
|
||||
# that could not have touched it (2258's session). A marker's group is the *edge it fires on*.
|
||||
# The in-game UI and the keybinding table (`seed_bindings_for_vm`, run on the world-entry edge)
|
||||
# build once per login; one count for two logins means the second reused the first's frame tree.
|
||||
for marker in "UIParent.xml loaded" "commands registered"; do
|
||||
n="$(printf '%s\n' "$plain" | grep -cF "$marker")"
|
||||
printf ' %-24s %s\n' "$marker" "$n"
|
||||
@@ -220,22 +143,15 @@ for marker in "UIParent.xml loaded" "commands registered"; do
|
||||
fail "'$marker' happened $n time(s), expected $sessions — once per login: the UI rebuilt per login (1290), the keybinding table seeded on the entry edge (2241)"
|
||||
done
|
||||
|
||||
# **The shutdown tail ran on BOTH roots** (decision 1528). One write is the `/logout`; the second is
|
||||
# the exit, and the smoke now exits the way a player does — by closing the window. That is the only
|
||||
# exit that tests whether the tail is reachable at all: the close button's `AppExit` is written in
|
||||
# `PostUpdate`, so for as long as the shutdown systems read it in `Update` this count was 1 and
|
||||
# every saved variable, every addon's file and the camera pose died with the process. Counted off
|
||||
# the flat file's own line: it is only written when the UI really loaded, which is the property the
|
||||
# sentinel needs. (It is no longer the tail's FIRST write — 2029 put the layout cache ahead of it,
|
||||
# in the reference's own slot — but it is still the first that says the session had a UI.)
|
||||
# The shutdown tail runs once per session: at the /logout, and at the exit, which closes the window
|
||||
# as a player does. Counted off the saved-variables line, written only when the UI loaded.
|
||||
writes="$(printf '%s\n' "$plain" | grep -cF "saved variables: wrote")"
|
||||
printf ' %-24s %s\n' "shutdown writes" "$writes"
|
||||
[ "$writes" -eq "$sessions" ] ||
|
||||
fail "the shutdown tail wrote $writes time(s), expected $sessions — a session ended without \
|
||||
saving (1528: the quit root must be observed in \`Last\`, after PostUpdate's exit_on_all_closed)"
|
||||
|
||||
# The read-only verdict (decision 1486). Reported by name: "something wrote to the install" is a
|
||||
# rule violation somebody has to go and find, and the file that appeared is the whole lead.
|
||||
# The read-only verdict names each changed file, the whole lead to whatever wrote it.
|
||||
if [ -n "$install_root" ]; then
|
||||
after="$(mktemp "${TMPDIR:-/tmp}/benilla-smoke-after.XXXXXX")"
|
||||
find -L "$install_root" -type f 2>/dev/null | sort >"$after"
|
||||
@@ -265,21 +181,13 @@ else
|
||||
echo " install not watched (no WoW link and no WOW_DATA — the read-only rule went unmeasured)"
|
||||
fi
|
||||
|
||||
# ── The realm-list leg (2069) ────────────────────────────────────────────────────────────────
|
||||
#
|
||||
# A second, short run, because it crosses a boundary the round trip above never touches: the realm
|
||||
# list is a dialog raised **over** character select, and the IO thread has to keep serving it from
|
||||
# the character park it is already sitting in. The version that ended the cycle there shipped, and
|
||||
# the failure was invisible to every unit test in the workspace — the app was stranded on one
|
||||
# screen while the thread walked two parks ahead, and the symptom the director saw was a client
|
||||
# that hung on "Connecting" with nothing in the log at all. This drives Change Realm → Okay →
|
||||
# Change Realm → Cancel → Okay against the real server and refuses anything but a completed walk.
|
||||
# ── The realm-list leg ───────────────────────────────────────────────────────────────────────
|
||||
# The realm list is a dialog over character select that the IO thread serves from the character
|
||||
# park; this drives Change Realm, Okay, Change Realm, Cancel, Okay against the real server and
|
||||
# fails anything but a completed walk.
|
||||
echo "smoke: running the realm-list boundary walk (~12 s, opens a window)…"
|
||||
rlog="$(mktemp "${TMPDIR:-/tmp}/benilla-smoke-realm.XXXXXX")"
|
||||
# **No `WOW_CHAR` here, deliberately** — an absent variable is invisible, so it is named instead.
|
||||
# The walk drives character select; the fast path would seat a body and there would be no roster
|
||||
# screen left to drive. The scrub at the top of this script is what makes the absence real, and
|
||||
# `realm_select::smoke` refuses on its own if one reaches it anyway.
|
||||
# No WOW_CHAR: the walk drives character select, and `realm_select::smoke` refuses a seated body.
|
||||
WOW_UNATTENDED=1 WOW_NOSOUND=1 WOW_USER="$user" WOW_PASS="$pass" WOW_REALM_SMOKE=1 \
|
||||
timeout 120 cargo run -q -p benilla >"$rlog" 2>&1
|
||||
rcode=$?
|
||||
|
||||
+5
-13
@@ -1,19 +1,11 @@
|
||||
#!/usr/bin/env bash
|
||||
# soundprobe.sh — start the client in MEASURING MODE (decision 1556).
|
||||
# Start the client in sound measuring mode: record the mix before and after the output limiter.
|
||||
#
|
||||
# The output limiter (1551) was proven offline and shipped on, and the director then played a real
|
||||
# session and heard no change. That gap is not settled by another fix; it is settled by a capture
|
||||
# from the machine and the encounter in question. This starts a run that records one.
|
||||
# scripts/soundprobe.sh [client args…] # play normally, press F9 when you hear it
|
||||
# scripts/soundprobe.py # …then read the capture back
|
||||
#
|
||||
# Builds with the `play` profile on purpose — the same profile the director actually plays in
|
||||
# (decision 1157). A debug build stutters, a stutter is a missed mix deadline, and a missed
|
||||
# deadline is one of the mechanisms under investigation: measuring on the wrong profile would
|
||||
# manufacture the very artifact the capture is meant to attribute.
|
||||
#
|
||||
# scripts/soundprobe.sh # play normally, press F9 when you hear it
|
||||
# scripts/soundprobe.py # …then read the capture back
|
||||
#
|
||||
# Any extra arguments are passed through to the client.
|
||||
# Builds the `play` profile: a debug build stutters, and a stutter is a missed mix deadline, one of
|
||||
# the mechanisms the capture has to tell apart.
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
|
||||
+15
-38
@@ -1,27 +1,11 @@
|
||||
#!/usr/bin/env bash
|
||||
# **The two-client live summon probe** (decision 1747) — the instrument that closes the loop on
|
||||
# being summoned, because nothing smaller can.
|
||||
# Two-client live summon probe: a second account summons this checkout's character, which accepts.
|
||||
#
|
||||
# The summon flow is the one confirm in the client that a single session cannot exercise at all:
|
||||
# vmangos has exactly two `SendSummonRequest` callers (`Spell::EffectSummonPlayer` and
|
||||
# `HandleGroupSummonCommand`), and **both skip you** — the ritual needs a warlock plus two
|
||||
# clickers, and `.group summon` explicitly `continue`s past the caster. So the question can only
|
||||
# be asked by somebody else, and the whole seam behind it (apply → feed → VM → drain → wire) is
|
||||
# exactly the part unit tests cannot reach.
|
||||
#
|
||||
# What it does: a summoner client on a second account invites this checkout's probe character
|
||||
# into a group and runs `.group summon`; the receiver reads its own CONFIRM_SUMMON dialog back
|
||||
# through `ProbeLog`, presses Accept, and re-reads its zone once the teleport has landed. The
|
||||
# receiver also writes the `summon` trace tag, so all three links report:
|
||||
#
|
||||
# summon recv SMSG_SUMMON_REQUEST summoner=… zone=… delay_ms=… dead_or_ghost=false
|
||||
# summon fire CONFIRM_SUMMON summoner=… zone=…
|
||||
# summon SEND CMSG_SUMMON_RESPONSE summoner=… n=1
|
||||
#
|
||||
# **The summoner is a second account the shell names** — `SUMMON_USER`, `SUMMON_PASS`,
|
||||
# `SUMMON_CHAR`, a GM-level test account whose login kicks nobody. The summoner's run carries
|
||||
# `WOW_ALLOW_ACCOUNT=1`, because it is not the account this checkout declares; that the shell
|
||||
# named it is what makes the override legitimate.
|
||||
# Both vmangos summon-request callers skip the caster (`Spell::EffectSummonPlayer`,
|
||||
# `HandleGroupSummonCommand`), so a second client summons. The summoner is SUMMON_USER/
|
||||
# SUMMON_PASS/SUMMON_CHAR, a GM test account whose login kicks nobody; the receiver is
|
||||
# `.probe-identity`, else WOW_USER/WOW_PASS/WOW_CHAR. WOW_HOST is the server, SUMMON_LIVE_KEEP=1
|
||||
# keeps the logs.
|
||||
#
|
||||
# SUMMON_USER=… SUMMON_PASS=… SUMMON_CHAR=… scripts/summon-live.sh # ~90 s, two small windows
|
||||
set -uo pipefail
|
||||
@@ -31,13 +15,11 @@ root="$(git -C "$PWD" rev-parse --show-toplevel 2>/dev/null || true)"
|
||||
cd "$root" || exit 1
|
||||
echo "summon-live: $root"
|
||||
|
||||
# The receiver is this run's own identity — smoke.sh's rule (scripts/probe-identity.sh), for its
|
||||
# reason: a login kicks whoever holds the account.
|
||||
# The receiver logs in as this checkout's identity: a login kicks whoever holds the account.
|
||||
. "$root/scripts/probe-identity.sh"
|
||||
probe_identity summon-live "$root" || exit 1
|
||||
rx_user="$PROBE_USER"; rx_pass="$PROBE_PASS"; rx_char="$PROBE_CHAR"
|
||||
|
||||
# The summoner is the shell's to name (see the header).
|
||||
if [ -z "${SUMMON_USER:-}" ] || [ -z "${SUMMON_PASS:-}" ] || [ -z "${SUMMON_CHAR:-}" ]; then
|
||||
echo "summon-live: REFUSING — the summoner needs a second account: SUMMON_USER, SUMMON_PASS"
|
||||
echo " and SUMMON_CHAR, a GM-level test account whose login kicks nobody."
|
||||
@@ -48,16 +30,14 @@ echo "summon-live: $tx_char ($tx_user) summons $rx_char ($rx_user)"
|
||||
|
||||
probe_server_or_skip summon-live || exit 0
|
||||
|
||||
# Build once, so the two `cargo run`s below start together instead of one waiting on the other's
|
||||
# build lock — the timings are wall-clock from process start (smoke.sh's build-then-time rule).
|
||||
# Build first so both clients start together: the probe timings count from process start.
|
||||
echo "summon-live: building…"
|
||||
cargo build -q -p benilla || { echo "summon-live: the client did not build"; exit 1; }
|
||||
|
||||
work="$(mktemp -d "${TMPDIR:-/tmp}/benilla-summon.XXXXXX")"
|
||||
trap '[ -n "${SUMMON_LIVE_KEEP:-}" ] || rm -rf "$work"' EXIT
|
||||
|
||||
# The receiver's chunk. `tostring` on every getter on purpose: a missing dialog must REPORT rather
|
||||
# than raise at line 2, or a failed run says "attempt to concatenate a nil value" and names nothing.
|
||||
# The receiver's Lua: `tostring` on every getter, so a missing dialog reports instead of raising.
|
||||
read -r -d '' chunk <<'LUA'
|
||||
ProbeLog("dialog visible=" .. tostring(StaticPopup1:IsVisible()))
|
||||
ProbeLog("dialog text=[" .. tostring(StaticPopup1Text:GetText()) .. "]")
|
||||
@@ -81,10 +61,8 @@ end)
|
||||
LUA
|
||||
|
||||
echo "summon-live: running (~90 s, opens two windows)…"
|
||||
# **The receiver is parked first, and the run is worthless without it.** Its character starts
|
||||
# wherever the last run left it — which, after one green run, is the destination — so a
|
||||
# before/after zone compare silently stops discriminating on the second run. It did exactly that.
|
||||
# Parking in Stormwind makes "before" the same on every run, whatever the previous one did.
|
||||
# The receiver parks in Stormwind first: a green run leaves it at the destination, and the
|
||||
# before/after zone compare needs the same "before" on every run.
|
||||
WOW_UNATTENDED=1 WOW_NOSOUND=1 WOW_USER="$rx_user" WOW_PASS="$rx_pass" WOW_CHAR="$rx_char" \
|
||||
WOW_PROBE=partner \
|
||||
WOW_PROBE_CHAT=".go xyz -8913 554 94" WOW_PROBE_CHAT_AT=10 \
|
||||
@@ -94,8 +72,8 @@ WOW_UNATTENDED=1 WOW_NOSOUND=1 WOW_USER="$rx_user" WOW_PASS="$rx_pass" WOW_CHAR=
|
||||
timeout 150 cargo run -q -p benilla >"$work/rx.log" 2>&1 &
|
||||
rx=$!
|
||||
|
||||
# `WOW_ALLOW_ACCOUNT=1` is the account guard's own escape hatch, and the shell naming the account
|
||||
# is what earns it. `.group summon` rather than `.summon`: the latter teleports without ever asking.
|
||||
# WOW_ALLOW_ACCOUNT=1 lifts the account guard for the shell-named summoner. `.group summon`, not
|
||||
# `.summon`, because `.summon` teleports without asking.
|
||||
WOW_UNATTENDED=1 WOW_NOSOUND=1 WOW_USER="$tx_user" WOW_PASS="$tx_pass" WOW_CHAR="$tx_char" WOW_ALLOW_ACCOUNT=1 \
|
||||
WOW_PROBE_CHAT="/invite $rx_char;.group summon" \
|
||||
WOW_PROBE_CHAT_AT=18 WOW_PROBE_CHAT_EVERY=10 \
|
||||
@@ -128,9 +106,8 @@ printf '%s' "$trace" | grep -q "recv SMSG_SUMMON_REQUEST" ||
|
||||
printf '%s' "$trace" | grep -q "fire CONFIRM_SUMMON" || fail "the request landed but no event fired"
|
||||
printf '%s' "$trace" | grep -q "SEND CMSG_SUMMON_RESPONSE" || fail "Accept sent no packet"
|
||||
printf '%s' "$plog" | grep -q "wants to summon you to" || fail "the dialog text never composed"
|
||||
# The two that cannot be faked by any amount of client-side bookkeeping: the character actually
|
||||
# moved, and it moved to **the place the dialog named**. The second is what ties
|
||||
# GetSummonConfirmAreaName to reality rather than to our own AreaTable lookup agreeing with itself.
|
||||
# The character must move, and land where the dialog said: that ties GetSummonConfirmAreaName to
|
||||
# the server rather than to our own AreaTable lookup.
|
||||
before="$(printf '%s' "$plog" | sed -n 's/^zone before=\[\(.*\)\]$/\1/p')"
|
||||
after="$(printf '%s' "$plog" | sed -n 's/^zone after=\[\(.*\)\]$/\1/p')"
|
||||
area="$(printf '%s' "$plog" | sed -n 's/^area=\[\(.*\)\]$/\1/p')"
|
||||
|
||||
+11
-36
@@ -1,21 +1,15 @@
|
||||
#!/usr/bin/env bash
|
||||
# Phase-5 visual A/B render harness driver (decisions 0008 + 0010).
|
||||
# Visual A/B harness: capture deterministic screenshots and diff them against a baseline.
|
||||
#
|
||||
# Captures deterministic benilla screenshots (one process per scenario, server-less + clutter-off +
|
||||
# pinned camera, game clock AND frame step — see the `capture` module) and diffs them with
|
||||
# `benilla-visual`. `selfcheck` is the proof that "deterministic" is true. The intended
|
||||
# loop: capture `baseline` on the current pipeline, make a render change, then `diff` — the diff is the
|
||||
# machine check that catches a regression before the director's eye (decision 0008).
|
||||
# One client process per scenario: server-less, clutter off, a pinned camera, a fixed game clock and
|
||||
# frame step (the `capture` module); `benilla-visual` diffs the PNGs. Opens a real window. Captures
|
||||
# hold Blizzard-derived imagery, so they stay under the gitignored target/visual/.
|
||||
#
|
||||
# Captures contain Blizzard-derived imagery, so they live under the gitignored target/visual/ and are
|
||||
# NEVER committed. macOS/local only: capture opens a real window (no headless GPU path here).
|
||||
#
|
||||
# Usage:
|
||||
# scripts/visual.sh list # print scenario names
|
||||
# scripts/visual.sh capture <dir> # capture every scenario into <dir>/
|
||||
# scripts/visual.sh baseline # capture into target/visual/baseline/
|
||||
# scripts/visual.sh diff [--fail <mae>] # capture into target/visual/candidate/, diff vs baseline/
|
||||
# scripts/visual.sh selfcheck # capture the sweep TWICE from ONE build; demand identical
|
||||
# scripts/visual.sh diff [--fail <mae>] # capture target/visual/candidate/, diff vs baseline/
|
||||
# scripts/visual.sh selfcheck [<mae>] # capture twice from one build; demand identical
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
@@ -23,8 +17,7 @@ VIS=target/visual
|
||||
|
||||
build() { cargo build -q -p benilla -p benilla-visual; }
|
||||
|
||||
# The scenario list is owned by the binary (capture/mod.rs), printed via WOW_CAPTURE=list — so this driver
|
||||
# never drifts from the code.
|
||||
# The binary owns the scenario list (`capture/mod.rs`) and prints it under WOW_CAPTURE=list.
|
||||
scenarios() { WOW_CAPTURE=list cargo run -q -p benilla; }
|
||||
|
||||
capture_into() {
|
||||
@@ -60,32 +53,14 @@ case "$cmd" in
|
||||
./target/debug/benilla-visual diff-dir "$VIS/baseline" "$VIS/candidate" --out "$VIS/diff" "$@"
|
||||
;;
|
||||
selfcheck) # selfcheck [<mae>] — the tolerance defaults to 0 (bit-identical)
|
||||
# The harness's own tripwire (decision 0723). A golden diff is evidence ONLY if the harness is
|
||||
# deterministic, so measure that directly: capture the whole sweep twice from one unchanged
|
||||
# build and demand a bit-identical result (--fail 0 passes only at MAE exactly 0). Before 0723
|
||||
# this failed at MAE 0.009 / max delta 180 — the same band a real render change lands in, which
|
||||
# is how 0721's flame pixels ended up unreadable and how 0719 read signal out of churn. Run it
|
||||
# after touching anything the capture clock feeds, and whenever a diff looks like noise.
|
||||
# A diff is evidence only if capture is deterministic: two sweeps from one build must match bit
|
||||
# for bit. Run it after touching anything the capture clock feeds.
|
||||
build
|
||||
capture_into "$VIS/self-a"
|
||||
capture_into "$VIS/self-b"
|
||||
echo "--- selfcheck: self-a vs self-b, one build, two runs ---"
|
||||
# The bar is 0 — bit-identical — and the four blessed scenarios meet it (0817 set the bar, 1183
|
||||
# cut the sweep to two director-framed spots x two day times). Scenarios that could not hold it
|
||||
# left the sweep rather than being carried as slack in the bar (the old 0.001 was cover for two
|
||||
# overlook cells at 421 px / max delta 43).
|
||||
#
|
||||
# What makes a cell flake is a real renderer defect, not harness drift: draw order follows spawn
|
||||
# order, and spawn order varies because asset loads complete on a thread pool (0723 "Open, named"
|
||||
# -> 0815 Open -> 1182, which measured it). Both states are perfectly STABLE, so no amount of
|
||||
# settling or waiting fixes it. **Do not respond to a failure here by widening the bar or the
|
||||
# waits** — that mistake has cost several sessions. Anything above 0 is either that defect
|
||||
# surfacing in a blessed cell or a genuine harness regression; pass a number to probe.
|
||||
#
|
||||
# 1182 is also the correction to the obvious first guess: the tie is NOT always a coplanar pair.
|
||||
# In the canal cells (deleted by 1183) it was two surfaces INTERSECTING, 0.62 yd apart, tying at
|
||||
# one MSAA sample — invisible with WOW_MSAA=off. Run WOW_PICK through the differing pixels and
|
||||
# read the PERPENDICULAR gap before assuming which geometry you are chasing.
|
||||
# Keep the bar at 0 and add no waits: a nonzero result is a harness regression or a renderer
|
||||
# defect, such as draw order following spawn order, which varies with thread-pool asset loads.
|
||||
./target/debug/benilla-visual diff-dir "$VIS/self-a" "$VIS/self-b" --out "$VIS/self-diff" \
|
||||
--fail "${2:-0}"
|
||||
echo "selfcheck OK — the harness clock is a pure function of the build"
|
||||
|
||||
Reference in New Issue
Block a user