Complete Stats validation, comparison cards, and annual ranking hardening
This commit is contained in:
@@ -2,6 +2,14 @@ $ErrorActionPreference = "Stop"
|
||||
|
||||
Write-Host "HLL Vietnam platform validation"
|
||||
|
||||
function Assert-LastExitCode {
|
||||
param([string] $Message)
|
||||
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw $Message
|
||||
}
|
||||
}
|
||||
|
||||
$configPath = "ai-platform.json"
|
||||
if (-not (Test-Path $configPath)) {
|
||||
throw "Missing $configPath"
|
||||
@@ -54,8 +62,12 @@ if status is None or payload.get("status") != "ok":
|
||||
'@
|
||||
|
||||
$backendImportCheck | python -
|
||||
Assert-LastExitCode "Backend startup import check failed."
|
||||
|
||||
powershell -ExecutionPolicy Bypass -File scripts/run-historical-ui-regression-tests.ps1
|
||||
Assert-LastExitCode "Historical UI regression validation failed."
|
||||
powershell -ExecutionPolicy Bypass -File scripts/run-stats-validation.ps1
|
||||
Assert-LastExitCode "Stats regression validation failed."
|
||||
|
||||
Write-Host "No product integration tests are configured for this platform-only scope."
|
||||
Write-Host "Backend startup import check passed."
|
||||
|
||||
226
scripts/run-stats-validation.ps1
Normal file
226
scripts/run-stats-validation.ps1
Normal file
@@ -0,0 +1,226 @@
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
Write-Host "Stats regression validation"
|
||||
|
||||
function Assert-FileExists {
|
||||
param(
|
||||
[string] $Path,
|
||||
[string] $Message
|
||||
)
|
||||
|
||||
if (-not (Test-Path $Path)) {
|
||||
throw $Message
|
||||
}
|
||||
}
|
||||
|
||||
function Assert-Contains {
|
||||
param(
|
||||
[string] $Content,
|
||||
[string] $Pattern,
|
||||
[string] $Message
|
||||
)
|
||||
|
||||
if ($Content -notmatch [regex]::Escape($Pattern)) {
|
||||
throw $Message
|
||||
}
|
||||
}
|
||||
|
||||
function Get-HttpStatusCode {
|
||||
param([string] $Url)
|
||||
|
||||
try {
|
||||
$response = Invoke-WebRequest -Uri $Url -UseBasicParsing -TimeoutSec 5
|
||||
return [int] $response.StatusCode
|
||||
} catch [System.Net.WebException] {
|
||||
if ($_.Exception.Response) {
|
||||
return [int] $_.Exception.Response.StatusCode
|
||||
}
|
||||
throw
|
||||
}
|
||||
}
|
||||
|
||||
function Assert-LastExitCode {
|
||||
param([string] $Message)
|
||||
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw $Message
|
||||
}
|
||||
}
|
||||
|
||||
Assert-FileExists "frontend/stats.html" "Missing frontend/stats.html"
|
||||
Assert-FileExists "frontend/assets/js/stats.js" "Missing frontend/assets/js/stats.js"
|
||||
|
||||
$statsHtml = Get-Content -Raw "frontend/stats.html"
|
||||
$statsJs = Get-Content -Raw "frontend/assets/js/stats.js"
|
||||
|
||||
Assert-Contains $statsHtml 'id="stats-search-form"' `
|
||||
"Stats page no longer exposes the player search form."
|
||||
Assert-Contains $statsHtml 'id="stats-profile-panel"' `
|
||||
"Stats page no longer exposes the player profile panel."
|
||||
Assert-Contains $statsHtml 'id="stats-annual-form"' `
|
||||
"Stats page no longer exposes the annual ranking form."
|
||||
Assert-Contains $statsHtml './assets/js/stats.js' `
|
||||
"Stats page no longer loads the Stats JavaScript asset."
|
||||
Assert-Contains $statsJs "/api/stats/players/search" `
|
||||
"Stats frontend no longer targets the player search endpoint."
|
||||
Assert-Contains $statsJs "/api/stats/rankings/annual" `
|
||||
"Stats frontend no longer targets the annual ranking endpoint."
|
||||
Assert-Contains $statsJs "/api/stats/players/" `
|
||||
"Stats frontend no longer targets the player profile endpoint."
|
||||
Assert-Contains $statsJs "Backend no disponible" `
|
||||
"Stats frontend no longer exposes the controlled backend-unavailable messaging."
|
||||
|
||||
$backendContractCheck = @'
|
||||
import json
|
||||
import sys
|
||||
from datetime import datetime, timezone
|
||||
|
||||
sys.path.insert(0, "backend")
|
||||
|
||||
from app.routes import resolve_get_payload
|
||||
|
||||
|
||||
def require(condition, message):
|
||||
if not condition:
|
||||
raise SystemExit(message)
|
||||
|
||||
|
||||
def read_payload(path):
|
||||
status, payload = resolve_get_payload(path)
|
||||
require(status is not None, f"{path} did not resolve")
|
||||
return int(status), payload
|
||||
|
||||
|
||||
health_status, health_payload = read_payload("/health")
|
||||
require(health_status == 200, "Route resolver /health no longer returns 200.")
|
||||
require(health_payload.get("status") == "ok", "Route resolver /health no longer returns ok status.")
|
||||
|
||||
search_status, search_payload = read_payload("/api/stats/players/search?q=regression-check&limit=5")
|
||||
require(search_status == 200, "Stats player search should return 200 for a valid query.")
|
||||
search_data = search_payload.get("data") or {}
|
||||
require(search_payload.get("status") == "ok", "Stats player search payload no longer returns ok status.")
|
||||
require(search_data.get("query") == "regression-check", "Stats player search no longer echoes query.")
|
||||
require(search_data.get("server_id") == "all-servers", "Stats player search no longer defaults server scope to all-servers.")
|
||||
require(isinstance(search_data.get("items"), list), "Stats player search items must remain a list.")
|
||||
|
||||
missing_query_status, missing_query_payload = read_payload("/api/stats/players/search")
|
||||
require(missing_query_status == 400, "Stats player search without q must return 400.")
|
||||
require("required" in str(missing_query_payload.get("message", "")).lower(), "Missing-q error message changed unexpectedly.")
|
||||
|
||||
invalid_search_limit_status, _ = read_payload("/api/stats/players/search?q=regression-check&limit=0")
|
||||
require(invalid_search_limit_status == 400, "Stats player search with limit=0 must return 400.")
|
||||
|
||||
profile_status, profile_payload = read_payload("/api/stats/players/regression-player?timeframe=weekly")
|
||||
require(profile_status == 200, "Stats player profile should return 200 for a valid player lookup.")
|
||||
profile_data = profile_payload.get("data") or {}
|
||||
require(profile_payload.get("status") == "ok", "Stats player profile payload no longer returns ok status.")
|
||||
require(profile_data.get("player_id") == "regression-player", "Stats player profile no longer preserves player id.")
|
||||
require(profile_data.get("timeframe") == "weekly", "Stats player profile no longer preserves timeframe.")
|
||||
require(profile_data.get("server_id") == "all-servers", "Stats player profile no longer defaults server scope to all-servers.")
|
||||
require(isinstance(profile_data.get("matches_considered"), int), "Stats player profile matches_considered must remain an int.")
|
||||
require(isinstance(profile_data.get("source"), dict), "Stats player profile source metadata must remain present.")
|
||||
require(isinstance(profile_data.get("weekly_ranking"), (dict, type(None))), "Stats player profile weekly ranking shape changed unexpectedly.")
|
||||
require(isinstance(profile_data.get("monthly_ranking"), (dict, type(None))), "Stats player profile monthly ranking shape changed unexpectedly.")
|
||||
|
||||
invalid_timeframe_status, _ = read_payload("/api/stats/players/regression-player?timeframe=seasonal")
|
||||
require(invalid_timeframe_status == 400, "Stats player profile with invalid timeframe must return 400.")
|
||||
|
||||
current_year = datetime.now(timezone.utc).year
|
||||
annual_status, annual_payload = read_payload(
|
||||
f"/api/stats/rankings/annual?year={current_year}&server_id=all&metric=kills&limit=20"
|
||||
)
|
||||
require(annual_status == 200, "Annual ranking should return 200 for metric=kills.")
|
||||
annual_data = annual_payload.get("data") or {}
|
||||
require(annual_payload.get("status") == "ok", "Annual ranking payload no longer returns ok status.")
|
||||
require(annual_data.get("year") == current_year, "Annual ranking no longer preserves requested year.")
|
||||
require(annual_data.get("server_id") == "all-servers", "Annual ranking no longer preserves the normalized all-servers scope.")
|
||||
require(annual_data.get("metric") == "kills", "Annual ranking metric changed unexpectedly.")
|
||||
require(
|
||||
isinstance(annual_data.get("limit"), int) and 1 <= annual_data.get("limit") <= 20,
|
||||
"Annual ranking limit must remain a positive int capped by the requested value.",
|
||||
)
|
||||
require(annual_data.get("snapshot_status") in {"ready", "missing"}, "Annual ranking snapshot status must remain ready or missing.")
|
||||
require(isinstance(annual_data.get("items"), list), "Annual ranking items must remain a list.")
|
||||
|
||||
low_limit_status, low_limit_payload = read_payload(
|
||||
f"/api/stats/rankings/annual?year={current_year}&server_id=all&metric=kills&limit=3"
|
||||
)
|
||||
require(low_limit_status == 200, "Annual ranking with low limit must return 200.")
|
||||
low_limit_value = (low_limit_payload.get("data") or {}).get("limit")
|
||||
require(
|
||||
isinstance(low_limit_value, int) and 1 <= low_limit_value <= 3,
|
||||
"Annual ranking limit normalization for low limits changed unexpectedly.",
|
||||
)
|
||||
|
||||
high_limit_status, _ = read_payload(
|
||||
f"/api/stats/rankings/annual?year={current_year}&server_id=all&metric=kills&limit=101"
|
||||
)
|
||||
require(high_limit_status == 400, "Annual ranking with limit=101 must return 400.")
|
||||
|
||||
unsupported_metric_status, unsupported_metric_payload = read_payload(
|
||||
f"/api/stats/rankings/annual?year={current_year}&server_id=all&metric=deaths&limit=20"
|
||||
)
|
||||
require(unsupported_metric_status == 400, "Annual ranking with unsupported metric must return 400.")
|
||||
require("metric" in str(unsupported_metric_payload.get("message", "")).lower(), "Unsupported metric error message changed unexpectedly.")
|
||||
|
||||
missing_year_status, _ = read_payload("/api/stats/rankings/annual?year=2999&server_id=all&metric=kills&limit=20")
|
||||
require(missing_year_status == 200, "Annual ranking future-year missing snapshot must still return 200.")
|
||||
|
||||
print(json.dumps({
|
||||
"checked": [
|
||||
"health",
|
||||
"stats-player-search",
|
||||
"stats-player-profile",
|
||||
"stats-annual-ranking",
|
||||
],
|
||||
"annual_snapshot_status": annual_data.get("snapshot_status"),
|
||||
"search_items_count": len(search_data.get("items") or []),
|
||||
"profile_matches_considered": profile_data.get("matches_considered"),
|
||||
}))
|
||||
'@
|
||||
|
||||
$backendContractCheck | python -
|
||||
Assert-LastExitCode "Stats route-contract validation failed."
|
||||
|
||||
$backendBaseUrl = "http://127.0.0.1:8000"
|
||||
$backendAvailable = $false
|
||||
|
||||
try {
|
||||
$healthPayload = Invoke-RestMethod -Uri "$backendBaseUrl/health" -TimeoutSec 5
|
||||
if ($healthPayload.status -ne "ok") {
|
||||
throw "Live backend health payload did not return status=ok."
|
||||
}
|
||||
$backendAvailable = $true
|
||||
Write-Host "Live backend available at $backendBaseUrl"
|
||||
} catch {
|
||||
Write-Warning "Live backend unavailable at $backendBaseUrl. Route-contract checks passed via local Python imports."
|
||||
Write-Host "Next steps: start the backend, then rerun scripts/run-stats-validation.ps1 to verify live HTTP responses."
|
||||
Write-Host "Expected limited behavior while offline: stats.html should show backend-unavailable states for search, profile, and annual ranking."
|
||||
}
|
||||
|
||||
if ($backendAvailable) {
|
||||
$currentYear = (Get-Date).ToUniversalTime().Year
|
||||
$searchPayload = Invoke-RestMethod -Uri "$backendBaseUrl/api/stats/players/search?q=regression-check&limit=5" -TimeoutSec 5
|
||||
if ($searchPayload.status -ne "ok") {
|
||||
throw "Live stats search no longer returns status=ok."
|
||||
}
|
||||
|
||||
$profilePayload = Invoke-RestMethod -Uri "$backendBaseUrl/api/stats/players/regression-player?timeframe=weekly" -TimeoutSec 5
|
||||
if ($profilePayload.status -ne "ok") {
|
||||
throw "Live stats profile no longer returns status=ok."
|
||||
}
|
||||
|
||||
$annualPayload = Invoke-RestMethod -Uri "$backendBaseUrl/api/stats/rankings/annual?year=$currentYear&server_id=all&metric=kills&limit=20" -TimeoutSec 5
|
||||
if ($annualPayload.status -ne "ok") {
|
||||
throw "Live annual ranking no longer returns status=ok."
|
||||
}
|
||||
|
||||
$unsupportedMetricStatus = Get-HttpStatusCode -Url "$backendBaseUrl/api/stats/rankings/annual?year=$currentYear&server_id=all&metric=deaths&limit=20"
|
||||
if ($unsupportedMetricStatus -ne 400) {
|
||||
throw "Live annual ranking with unsupported metric must return HTTP 400."
|
||||
}
|
||||
|
||||
Write-Host "Live HTTP checks passed for Stats endpoints."
|
||||
}
|
||||
|
||||
Write-Host "Stats regression validation passed."
|
||||
Reference in New Issue
Block a user