#!/usr/bin/env bash # Test subcommands: data validation, full test pipeline, cross-language # golden-vector parity, coverage reports. # # Split out of dev.sh. `./run test` runs GUT + cargo + vitest + stability; # `./run test:golden` is the 3-consumer parity gate; `./run validate` # runs the JSON schema validator standalone. cmd_validate() { echo -e "${BLUE}Validating game data JSON schemas...${NC}" python3 "$REPO_ROOT/tools/validate-game-data.py" "$@" } # Run Rust workspace tests, preferring nextest when available. _cargo_test_workspace() { if _have_tool cargo-nextest "cargo install cargo-nextest --locked"; then (cd "$SIMULATOR_DIR" && cargo nextest run --workspace) else (cd "$SIMULATOR_DIR" && cargo test --workspace) fi } cmd_test() { local exit_code=0 echo -e "${BLUE}Running GUT tests (GDScript)...${NC}" WAYLAND_DISPLAY="${WAYLAND_DISPLAY:-wayland-0}" \ XDG_RUNTIME_DIR="${XDG_RUNTIME_DIR:-/run/user/$(id -u)}" \ $GODOT_BIN --path "$GAME_DIR" --headless --script res://addons/gut/gut_cmdln.gd \ -gexit "$@" || exit_code=$? echo "" echo -e "${BLUE}Running Rust tests (simulator)...${NC}" _cargo_test_workspace || exit_code=$? echo "" echo -e "${BLUE}Running vitest (guide)...${NC}" pnpm --prefix "$GUIDE_DIR" test || exit_code=$? echo "" echo -e "${BLUE}Running stability test (20s game boot)...${NC}" _run_stability_test || exit_code=$? return $exit_code } _run_stability_test() { # Boots the game → world_map, waits 20s, captures screenshot. # If the game crashes before capture, exit code is non-zero. local LOG="/tmp/stability_test_$$.log" cmd_screenshot "stability_test" "world_map" "20" > "$LOG" 2>&1 if [ $? -ne 0 ]; then echo -e "${RED}FAIL: Game crashed during stability test${NC}" grep -E "SCRIPT ERROR|ERROR:" "$LOG" | head -5 return 1 fi if grep -q "Captured:" "$LOG"; then echo -e "${GREEN}PASS: Game stable for 20s, screenshot captured${NC}" return 0 else echo -e "${RED}FAIL: Game ran but no screenshot captured${NC}" cat "$LOG" | tail -5 return 1 fi } cmd_coverage() { # Generate coverage reports for Rust + TypeScript. # Graceful degradation: each tool warn-skips if not installed. local exit_code=0 echo -e "${BLUE}[1/2] Rust coverage (cargo llvm-cov)...${NC}" if _have_tool cargo-llvm-cov "cargo install cargo-llvm-cov --locked"; then (cd "$SIMULATOR_DIR" && cargo llvm-cov --workspace --html) || exit_code=$? echo -e "${BLUE}HTML report: $SIMULATOR_DIR/target/llvm-cov/html/index.html${NC}" fi echo "" echo -e "${BLUE}[2/2] TypeScript coverage (pnpm -r test:coverage)...${NC}" # --if-present: pnpm exits 0 when no package defines the script, which is # the graceful-degrade behavior we want. Without it, pnpm exits 1 with # ERR_PNPM_RECURSIVE_RUN_NO_SCRIPT, which would falsely fail verify. pnpm -r --if-present run test:coverage || exit_code=$? return $exit_code } cmd_test_golden() { # Cross-language golden-vector parity gate. # # Each fixture in src/simulator/tests/golden/vectors/*.json is consumed by # three runners that MUST produce bitwise-identical output. Divergence = # release blocker (FFI marshaling / non-determinism / SOT violation). # # See src/simulator/tests/golden/README.md for the fixture shape and # ~/.claude/instructions/rust-code-standards.md §"Testing Strategy" for rationale. local vectors_dir="$SIMULATOR_DIR/tests/golden/vectors" local exit_code=0 if [ ! -d "$vectors_dir" ]; then echo -e "${RED}Golden vectors directory missing: $vectors_dir${NC}" return 1 fi local vectors vectors=$(find "$vectors_dir" -maxdepth 1 -name '*.json' -type f | sort) if [ -z "$vectors" ]; then echo -e "${YELLOW}No golden vectors yet — add JSON fixtures to:${NC}" echo -e " $vectors_dir" echo -e "${YELLOW}See $SIMULATOR_DIR/tests/golden/README.md for the fixture shape.${NC}" return 0 fi local count count=$(echo "$vectors" | wc -l | tr -d ' ') echo -e "${BLUE}Found $count golden vector(s) — running 3-consumer parity check${NC}" echo "" # Consumer 1: Rust native echo -e "${BLUE}[1/3] Rust native consumer (cargo test --test golden)${NC}" if ! (cd "$SIMULATOR_DIR" && cargo test --workspace --test golden 2>&1); then echo -e "${RED}FAIL: Rust golden tests${NC}" exit_code=1 fi echo "" # Consumer 2: WASM via Vitest (guide simulation worker) echo -e "${BLUE}[2/3] WASM consumer (pnpm test — golden suite)${NC}" if ! pnpm --prefix "$GUIDE_DIR" test -- --run golden 2>&1; then echo -e "${RED}FAIL: WASM golden tests${NC}" exit_code=1 fi echo "" # Consumer 3: GDExtension via headless Godot + GUT echo -e "${BLUE}[3/3] GDExtension consumer (headless Godot + GUT ffi/)${NC}" local ffi_dir="$GAME_DIR/engine/tests/ffi" if [ -d "$ffi_dir" ] && [ -n "$(find "$ffi_dir" -maxdepth 1 -name 'test_golden_*.gd' -print -quit 2>/dev/null)" ]; then WAYLAND_DISPLAY="${WAYLAND_DISPLAY:-wayland-0}" \ XDG_RUNTIME_DIR="${XDG_RUNTIME_DIR:-/run/user/$(id -u)}" \ $GODOT_BIN --path "$GAME_DIR" --headless \ --script res://addons/gut/gut_cmdln.gd \ -gdir=res://engine/tests/ffi -gprefix=test_golden_ -gexit 2>&1 \ || exit_code=$? else echo -e "${YELLOW}SKIP: No GDExt golden tests yet at $ffi_dir/test_golden_*.gd${NC}" fi echo "" if [ $exit_code -eq 0 ]; then echo -e "${GREEN}All 3 consumers agree on $count vector(s)${NC}" else echo -e "${RED}Divergence detected — release blocker${NC}" echo -e "${RED}See src/simulator/tests/golden/README.md for triage guidance${NC}" fi return $exit_code }