This commit is contained in:
112
scripts/codex-runner.ps1
Normal file
112
scripts/codex-runner.ps1
Normal file
@@ -0,0 +1,112 @@
|
||||
param(
|
||||
[int]$PollIntervalSeconds = 30
|
||||
)
|
||||
|
||||
$configPath = "ai-platform.json"
|
||||
|
||||
function Get-PlatformConfig {
|
||||
param(
|
||||
[string]$Path
|
||||
)
|
||||
|
||||
if (-not (Test-Path $Path)) {
|
||||
return $null
|
||||
}
|
||||
|
||||
try {
|
||||
return Get-Content -Raw $Path | ConvertFrom-Json
|
||||
}
|
||||
catch {
|
||||
Write-Host "Unable to read $Path. Falling back to default worker paths."
|
||||
return $null
|
||||
}
|
||||
}
|
||||
|
||||
$platformConfig = Get-PlatformConfig -Path $configPath
|
||||
$projectName = if ($platformConfig.project.name) { $platformConfig.project.name } else { "HLL Vietnam" }
|
||||
$taskPaths = $platformConfig.workflow.task_paths
|
||||
$runnerConfig = $platformConfig.runner
|
||||
|
||||
$pendingTasksPath = if ($taskPaths.pending) { $taskPaths.pending } else { "ai/tasks/pending" }
|
||||
$lockFile = if ($runnerConfig.lock_file) { $runnerConfig.lock_file } else { "ai/worker.lock" }
|
||||
$metricsFile = if ($runnerConfig.metrics_file) { $runnerConfig.metrics_file } else { "ai/system-metrics.md" }
|
||||
$integrationTestsScript = if ($runnerConfig.integration_tests_script) { $runnerConfig.integration_tests_script } else { "scripts/run-integration-tests.ps1" }
|
||||
$codexPrompt = if ($runnerConfig.codex_prompt) { $runnerConfig.codex_prompt } else { "Follow AGENTS.md, read the platform context in ai/, and process the pending tasks without acting outside task scope." }
|
||||
|
||||
Write-Host "$projectName Codex worker started..."
|
||||
|
||||
function Test-WorkerProcessActive {
|
||||
param(
|
||||
[string]$LockFilePath
|
||||
)
|
||||
|
||||
if (-not (Test-Path $LockFilePath)) {
|
||||
return $false
|
||||
}
|
||||
|
||||
$lockContent = Get-Content $LockFilePath -ErrorAction SilentlyContinue | Select-Object -First 1
|
||||
$workerPid = 0
|
||||
|
||||
if ([int]::TryParse(($lockContent -as [string]), [ref]$workerPid) -and $workerPid -gt 0) {
|
||||
try {
|
||||
Get-Process -Id $workerPid -ErrorAction Stop | Out-Null
|
||||
return $true
|
||||
}
|
||||
catch {
|
||||
return $false
|
||||
}
|
||||
}
|
||||
|
||||
return $false
|
||||
}
|
||||
|
||||
while ($true) {
|
||||
if (Test-Path $lockFile) {
|
||||
if (Test-WorkerProcessActive -LockFilePath $lockFile) {
|
||||
Write-Host "Worker already running. Waiting..."
|
||||
Start-Sleep -Seconds $PollIntervalSeconds
|
||||
continue
|
||||
}
|
||||
|
||||
Write-Host "Stale worker lock detected. Removing it."
|
||||
Remove-Item $lockFile -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
|
||||
$pendingTasks = Get-ChildItem $pendingTasksPath -Filter *.md -ErrorAction SilentlyContinue
|
||||
|
||||
if (-not $pendingTasks) {
|
||||
Write-Host "No pending tasks found."
|
||||
Start-Sleep -Seconds $PollIntervalSeconds
|
||||
continue
|
||||
}
|
||||
|
||||
if (-not (Get-Command codex -ErrorAction SilentlyContinue)) {
|
||||
Write-Host "Codex CLI not found. Install and authenticate Codex before running the worker."
|
||||
exit 1
|
||||
}
|
||||
|
||||
Set-Content -Path $lockFile -Value "$PID" -NoNewline
|
||||
|
||||
try {
|
||||
$startTime = Get-Date
|
||||
codex $codexPrompt
|
||||
$codexExitCode = $LASTEXITCODE
|
||||
$duration = [math]::Round(((Get-Date) - $startTime).TotalSeconds, 2)
|
||||
|
||||
if ($codexExitCode -eq 0) {
|
||||
Add-Content $metricsFile "$(Get-Date -Format s) | worker-cycle | $duration sec | success | codex-runner"
|
||||
|
||||
if (Test-Path $integrationTestsScript) {
|
||||
powershell -ExecutionPolicy Bypass -File $integrationTestsScript
|
||||
}
|
||||
}
|
||||
else {
|
||||
Add-Content $metricsFile "$(Get-Date -Format s) | worker-cycle | $duration sec | failed($codexExitCode) | codex-runner"
|
||||
}
|
||||
}
|
||||
finally {
|
||||
Remove-Item $lockFile -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
|
||||
Start-Sleep -Seconds $PollIntervalSeconds
|
||||
}
|
||||
95
scripts/run-historical-ui-regression-tests.ps1
Normal file
95
scripts/run-historical-ui-regression-tests.ps1
Normal file
@@ -0,0 +1,95 @@
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
Write-Host "Historical UI regression validation"
|
||||
|
||||
function Assert-Contains {
|
||||
param(
|
||||
[string] $Content,
|
||||
[string] $Pattern,
|
||||
[string] $Message
|
||||
)
|
||||
|
||||
if ($Content -notmatch [regex]::Escape($Pattern)) {
|
||||
throw $Message
|
||||
}
|
||||
}
|
||||
|
||||
function Assert-NotContains {
|
||||
param(
|
||||
[string] $Content,
|
||||
[string] $Pattern,
|
||||
[string] $Message
|
||||
)
|
||||
|
||||
if ($Content -match [regex]::Escape($Pattern)) {
|
||||
throw $Message
|
||||
}
|
||||
}
|
||||
|
||||
function Get-VisibleText {
|
||||
param([string] $Html)
|
||||
|
||||
return ($Html -replace "<script[\s\S]*?</script>", " " `
|
||||
-replace "<style[\s\S]*?</style>", " " `
|
||||
-replace "<[^>]+>", " ")
|
||||
}
|
||||
|
||||
$historicoHtml = Get-Content -Raw "frontend/historico.html"
|
||||
$historicoPartidaHtml = Get-Content -Raw "frontend/historico-partida.html"
|
||||
$historicoJs = Get-Content -Raw "frontend/assets/js/historico.js"
|
||||
$historicoPartidaJs = Get-Content -Raw "frontend/assets/js/historico-partida.js"
|
||||
$visibleHistoricalText = "$(Get-VisibleText $historicoHtml) $(Get-VisibleText $historicoPartidaHtml)"
|
||||
|
||||
Assert-NotContains $historicoHtml 'data-server-slug="comunidad-hispana-03"' `
|
||||
"Comunidad Hispana #03 selector was reintroduced."
|
||||
Assert-NotContains $historicoHtml "MVP mensual V1" `
|
||||
"MVP mensual V1 block was reintroduced in visible historical HTML."
|
||||
Assert-NotContains $historicoHtml "MVP mensual V2" `
|
||||
"MVP mensual V2 block was reintroduced in visible historical HTML."
|
||||
Assert-NotContains $historicoHtml "Comparativa V1 vs V2" `
|
||||
"Comparativa V1 vs V2 block was reintroduced in visible historical HTML."
|
||||
Assert-NotContains $historicoHtml "Elo/MMR" `
|
||||
"Elo/MMR public block was reintroduced in visible historical HTML."
|
||||
Assert-NotContains $visibleHistoricalText "snapshot" `
|
||||
"Public snapshot wording was reintroduced in visible historical text."
|
||||
|
||||
Assert-NotContains $historicoJs "Ver partida" `
|
||||
"Recent match cards must not expose the external scoreboard action label."
|
||||
Assert-Contains $historicoJs "Ver detalles" `
|
||||
"Recent match cards no longer include the internal detail fallback label."
|
||||
Assert-NotContains $historicoJs "item.match_url || item.source_url" `
|
||||
"Recent match cards must not trust legacy source_url fallback."
|
||||
Assert-Contains $historicoPartidaJs "Ver en Scoreboard" `
|
||||
"Match detail page no longer includes the external scoreboard action label."
|
||||
Assert-Contains $historicoPartidaJs 'rel="noopener noreferrer"' `
|
||||
"External scoreboard links must keep rel noopener noreferrer."
|
||||
Assert-Contains $historicoPartidaJs 'target="_blank"' `
|
||||
"External scoreboard links must open in a new tab."
|
||||
|
||||
$backendCheck = @'
|
||||
import sys
|
||||
sys.path.insert(0, "backend")
|
||||
|
||||
from app.config import get_historical_data_source_kind
|
||||
from app.routes import resolve_get_payload
|
||||
|
||||
status, payload = resolve_get_payload("/health")
|
||||
if status is None or payload.get("status") != "ok":
|
||||
raise SystemExit("/health did not resolve to an ok payload.")
|
||||
if payload.get("historical_data_source") != "rcon":
|
||||
raise SystemExit("/health no longer reports RCON-first historical source.")
|
||||
if get_historical_data_source_kind() != "rcon":
|
||||
raise SystemExit("Configured historical source is no longer rcon.")
|
||||
|
||||
detail_status, detail_payload = resolve_get_payload(
|
||||
"/api/historical/matches/detail?server=comunidad-hispana-01&match=regression-check"
|
||||
)
|
||||
if detail_status is None or detail_payload.get("status") != "ok":
|
||||
raise SystemExit("Match detail endpoint did not resolve successfully.")
|
||||
if detail_payload.get("data", {}).get("context") != "historical-match-detail":
|
||||
raise SystemExit("Match detail endpoint context changed unexpectedly.")
|
||||
'@
|
||||
|
||||
$backendCheck | python -
|
||||
|
||||
Write-Host "Historical UI regression validation passed."
|
||||
63
scripts/run-integration-tests.ps1
Normal file
63
scripts/run-integration-tests.ps1
Normal file
@@ -0,0 +1,63 @@
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
Write-Host "HLL Vietnam platform validation"
|
||||
|
||||
$configPath = "ai-platform.json"
|
||||
if (-not (Test-Path $configPath)) {
|
||||
throw "Missing $configPath"
|
||||
}
|
||||
|
||||
$config = Get-Content -Raw $configPath | ConvertFrom-Json
|
||||
$taskPaths = $config.workflow.task_paths
|
||||
|
||||
$requiredTaskPaths = @(
|
||||
$taskPaths.pending,
|
||||
$taskPaths.in_progress,
|
||||
$taskPaths.review,
|
||||
$taskPaths.blocked,
|
||||
$taskPaths.obsolete,
|
||||
$taskPaths.done
|
||||
)
|
||||
|
||||
foreach ($path in $requiredTaskPaths) {
|
||||
if (-not $path -or -not (Test-Path $path)) {
|
||||
throw "Missing task lifecycle path: $path"
|
||||
}
|
||||
}
|
||||
|
||||
$gitignore = Get-Content -Raw ".gitignore"
|
||||
$requiredIgnoreRules = @(
|
||||
"backend/runtime/",
|
||||
"ai/reports/*.md",
|
||||
"!ai/reports/.gitkeep"
|
||||
)
|
||||
|
||||
foreach ($rule in $requiredIgnoreRules) {
|
||||
if ($gitignore -notmatch [regex]::Escape($rule)) {
|
||||
throw "Missing .gitignore rule: $rule"
|
||||
}
|
||||
}
|
||||
|
||||
if (-not (Test-Path "ai/reports/.gitkeep")) {
|
||||
throw "Missing ai/reports/.gitkeep"
|
||||
}
|
||||
|
||||
$backendImportCheck = @'
|
||||
import sys
|
||||
sys.path.insert(0, "backend")
|
||||
import app.main
|
||||
from app.routes import resolve_get_payload
|
||||
|
||||
status, payload = resolve_get_payload("/health")
|
||||
if status is None or payload.get("status") != "ok":
|
||||
raise SystemExit("Backend health route did not resolve to an ok payload.")
|
||||
'@
|
||||
|
||||
$backendImportCheck | python -
|
||||
|
||||
powershell -ExecutionPolicy Bypass -File scripts/run-historical-ui-regression-tests.ps1
|
||||
|
||||
Write-Host "No product integration tests are configured for this platform-only scope."
|
||||
Write-Host "Backend startup import check passed."
|
||||
Write-Host "Platform validation passed."
|
||||
exit 0
|
||||
123
scripts/run-rcon-data-pipeline-tests.ps1
Normal file
123
scripts/run-rcon-data-pipeline-tests.ps1
Normal file
@@ -0,0 +1,123 @@
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
Write-Host "HLL Vietnam RCON data pipeline validation"
|
||||
|
||||
function Invoke-Step {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)][string]$Name,
|
||||
[Parameter(Mandatory = $true)][scriptblock]$Command
|
||||
)
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "== $Name =="
|
||||
& $Command
|
||||
}
|
||||
|
||||
function Test-PythonModule {
|
||||
param([Parameter(Mandatory = $true)][string]$ModuleName)
|
||||
|
||||
$check = "import importlib.util; raise SystemExit(0 if importlib.util.find_spec('$ModuleName') else 1)"
|
||||
python -c $check *> $null
|
||||
return $LASTEXITCODE -eq 0
|
||||
}
|
||||
|
||||
Invoke-Step "Compile backend application modules" {
|
||||
python -m compileall backend/app
|
||||
}
|
||||
|
||||
$previousPythonPath = $env:PYTHONPATH
|
||||
$env:PYTHONPATH = if ($previousPythonPath) { "backend;$previousPythonPath" } else { "backend" }
|
||||
|
||||
try {
|
||||
if (Test-PythonModule "pytest") {
|
||||
Invoke-Step "Run RCON parser, storage and materialization tests with pytest" {
|
||||
python -m pytest `
|
||||
backend/tests/test_rcon_admin_log_parser.py `
|
||||
backend/tests/test_rcon_admin_log_storage.py `
|
||||
backend/tests/test_rcon_materialization_pipeline.py `
|
||||
backend/tests/test_scoreboard_match_links.py
|
||||
}
|
||||
}
|
||||
else {
|
||||
Write-Host ""
|
||||
Write-Host "pytest is not installed; running offline fallback checks for RCON parser/storage and unittest suites."
|
||||
|
||||
Invoke-Step "Run RCON parser and storage fallback checks" {
|
||||
$fallbackChecks = @'
|
||||
from pathlib import Path
|
||||
import tempfile
|
||||
from backend.tests import test_rcon_admin_log_parser as parser_tests
|
||||
from backend.tests import test_rcon_admin_log_storage as storage_tests
|
||||
|
||||
parser_tests.test_parse_match_start()
|
||||
parser_tests.test_parse_match_end()
|
||||
parser_tests.test_parse_kill()
|
||||
parser_tests.test_parse_team_switch()
|
||||
parser_tests.test_parse_connected()
|
||||
parser_tests.test_parse_disconnected()
|
||||
parser_tests.test_parse_chat()
|
||||
parser_tests.test_parse_kick()
|
||||
parser_tests.test_parse_message_profile()
|
||||
parser_tests.test_parse_player_profile_snapshot_spanish_sections()
|
||||
parser_tests.test_non_profile_message_does_not_parse_as_profile_snapshot()
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
storage_tests.test_initialize_rcon_admin_log_storage_creates_event_table(Path(tmp))
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
storage_tests.test_persist_rcon_admin_log_entries_inserts_then_reports_duplicates(Path(tmp))
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
storage_tests.test_profile_message_snapshots_are_materialized_and_deduped(Path(tmp))
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
storage_tests.test_non_profile_messages_do_not_create_profile_snapshots(Path(tmp))
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
storage_tests.test_canonical_message_dedupes_changing_relative_prefixes(Path(tmp))
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
storage_tests.test_list_rcon_admin_log_event_counts_groups_by_target_and_event_type(Path(tmp))
|
||||
|
||||
print("RCON parser and storage fallback checks passed.")
|
||||
'@
|
||||
$fallbackChecks | python -
|
||||
}
|
||||
|
||||
Invoke-Step "Run RCON materialization unittest suite" {
|
||||
python -m unittest backend.tests.test_rcon_materialization_pipeline
|
||||
}
|
||||
|
||||
Invoke-Step "Run RCON scoreboard link unittest suite" {
|
||||
python -m unittest backend.tests.test_scoreboard_match_links
|
||||
}
|
||||
}
|
||||
}
|
||||
finally {
|
||||
$env:PYTHONPATH = $previousPythonPath
|
||||
}
|
||||
|
||||
Invoke-Step "Optional Docker backend smoke check" {
|
||||
if (-not (Get-Command docker -ErrorAction SilentlyContinue)) {
|
||||
Write-Host "Skipping Docker smoke check: docker command is not available."
|
||||
return
|
||||
}
|
||||
|
||||
docker compose ps --services --filter "status=running" *> $null
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Host "Skipping Docker smoke check: docker compose is not available or no compose project is active."
|
||||
return
|
||||
}
|
||||
|
||||
$runningServices = docker compose ps --services --filter "status=running"
|
||||
if ($runningServices -notcontains "backend") {
|
||||
Write-Host "Skipping backend endpoint smoke check: backend service is not running."
|
||||
return
|
||||
}
|
||||
|
||||
$health = Invoke-WebRequest "http://localhost:8000/health" -UseBasicParsing
|
||||
if ($health.StatusCode -lt 200 -or $health.StatusCode -ge 300) {
|
||||
throw "Backend health smoke check failed with status $($health.StatusCode)."
|
||||
}
|
||||
Write-Host "Backend health smoke check passed."
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "Skipping real RCON checks: this validation is designed to run without RCON credentials."
|
||||
Write-Host "RCON data pipeline validation passed."
|
||||
exit 0
|
||||
Reference in New Issue
Block a user