feat(engine): live server visual tests and gauntlet snapshot replay

Add live server lifecycle to tests/run-visual (start/stop server per
scenario, parse LISTENING:{port}). Add MessagePack snapshot replay to
visual_capture.gd via Protocol.decode_snapshot() — exercises the full
client pipeline from wire bytes to rendered fog. Three replay scenarios
(hub_spawn, fog_theater, hub_after_movement) plus one live scenario
(fog_live_hub). Add gen_gauntlet_fixtures.rs to produce .msgpack fixtures
from the Gauntlet test world. Add max_diff_pct threshold to visual-diff.
Makefile: add fixtures-gauntlet target, fix build-client double-import,
preserve .godot cache in clean.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-05 08:41:09 +01:00
co-authored by Claude Opus 4.6
parent 2189b00c6f
commit ac763fef97
16 changed files with 9034 additions and 9 deletions
+24 -7
View File
@@ -239,18 +239,22 @@ def compare(
# ---------------------------------------------------------------------------
def load_tolerance(config_path: Path | None) -> int:
"""Read tolerance from config JSON, return DEFAULT_TOLERANCE on failure."""
def load_config(config_path: Path | None) -> dict:
"""Read visual test config, return dict with tolerance and max_diff_pct."""
if config_path is None:
config_path = DEFAULT_CONFIG
defaults = {"tolerance": DEFAULT_TOLERANCE, "max_diff_pct": 0.0}
if not config_path.exists():
return DEFAULT_TOLERANCE
return defaults
try:
with open(config_path) as f:
data = json.load(f)
return int(data.get("tolerance", DEFAULT_TOLERANCE))
return {
"tolerance": int(data.get("tolerance", DEFAULT_TOLERANCE)),
"max_diff_pct": float(data.get("max_diff_pct", 0.0)),
}
except (json.JSONDecodeError, ValueError, OSError):
return DEFAULT_TOLERANCE
return defaults
# ---------------------------------------------------------------------------
@@ -270,6 +274,12 @@ def main() -> int:
default=None,
help="Per-channel pixel tolerance (default: from config or 5)",
)
parser.add_argument(
"--max-diff-pct",
type=float,
default=None,
help="Max allowed diff percentage (default: from config or 0.0)",
)
parser.add_argument(
"--diff-output",
default=None,
@@ -282,9 +292,11 @@ def main() -> int:
)
args = parser.parse_args()
# Resolve tolerance: CLI > config > fallback
# Resolve settings: CLI > config > fallback
config_path = Path(args.config) if args.config else None
tolerance = args.tolerance if args.tolerance is not None else load_tolerance(config_path)
cfg = load_config(config_path)
tolerance = args.tolerance if args.tolerance is not None else cfg["tolerance"]
max_diff_pct = args.max_diff_pct if args.max_diff_pct is not None else cfg["max_diff_pct"]
# Read images
try:
@@ -322,6 +334,11 @@ def main() -> int:
return 0
pct = diff_count / total * 100
if pct <= max_diff_pct:
print(f"PASS: {diff_count} of {total} pixels differ ({pct:.1f}%, within {max_diff_pct}% threshold)")
return 0
print(f"FAIL: {diff_count} of {total} pixels differ ({pct:.1f}%)")
if args.diff_output and diff_buf: