Files
TS_Proto/run-tests.ps1
T
nilesh.pandClaude Opus 4.8 45b5b34680 Add AI-assisted QA test workflow: scenario cards, runner, hooks, API capture
A complete loop for defining, generating, running and gating tests:

- qa/: scenario-card template + examples (API/UI/binding), the Claude
  generation prompt, and a QA runbook tying it together
- e2e/CLAUDE.md: conventions that keep AI-generated tests consistent
- run-tests.ps1: one runner for every layer (backend/api/web/ui/binding),
  logs per run under .testlogs/, JUnit output, evidence report, and a
  re-seed guard against stale demo data
- .githooks/ + scripts/install-hooks.ps1: pre-commit secret scan and
  pre-push unit tests (shift-left before CI)
- evidence report now captures each API call's request, payload and
  response (Authorization redacted); JUnit reporter added for CI
- scripts/db.mjs: db:check freshness guard

Azure DevOps pipeline deferred; the runner already emits JUnit + the
evidence report for any CI to publish.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-10 13:04:46 +05:30

253 lines
8.5 KiB
PowerShell

#requires -Version 5.1
<#
.SYNOPSIS
Run the TravelHub test suite by layer, log everything, and emit CI-ready
results and an evidence report. The same script runs locally, in a git
hook, and in a pipeline.
.DESCRIPTION
One runner for every test layer so there is a single source of truth for
"how tests run here". It:
- refreshes PATH from the registry (works in stale terminals)
- guards e2e against stale demo data by re-seeding when inventory is old
- writes a timestamped log per step under .testlogs\<run-id>\
- emits JUnit XML (e2e) that Azure DevOps / Gitea can publish
- regenerates the evidence report (screenshots + API request/response)
- returns a non-zero exit code if any layer fails, so CI can gate on it
Layers map to the scenario taxonomy in qa\:
backend Go unit + integration tests (repository, middleware, config)
api Go HTTP-contract tests (handlers, middleware)
web Angular unit tests
ui Playwright browser journeys (UI + binding, with API capture)
binding alias for ui -- the UI<->API seam, proven by captured traffic
unit backend + web (no browser; fast)
all backend + web + ui
.PARAMETER Layer
Which layer to run. Default: all.
.PARAMETER Grep
Only run tests whose title matches this (passed to `go test -run` and
`playwright -g`).
.PARAMETER Reseed
Force a database re-seed before running, instead of only when stale.
.PARAMETER NoReport
Skip regenerating the evidence report (faster; ui/binding only).
.PARAMETER Open
Open the evidence report in the browser when the run finishes.
.EXAMPLE
.\run-tests.ps1 -Layer all
.EXAMPLE
.\run-tests.ps1 -Layer ui -Grep "oversell" -Open
.EXAMPLE
.\run-tests.ps1 -Layer unit # fast pre-commit gate, no browser
#>
[CmdletBinding()]
param(
[ValidateSet('all', 'unit', 'backend', 'api', 'web', 'ui', 'binding')]
[string]$Layer = 'all',
[string]$Grep,
[switch]$Reseed,
[switch]$NoReport,
[switch]$Open
)
$ErrorActionPreference = 'Stop'
# --- configuration ---------------------------------------------------------
$Root = $PSScriptRoot
$Backend = Join-Path $Root 'backend'
$Frontend = Join-Path $Root 'frontend'
$E2E = Join-Path $Root 'e2e'
$RunId = Get-Date -Format 'yyyyMMdd-HHmmss'
$LogDir = Join-Path $Root ".testlogs\$RunId"
# --- console ---------------------------------------------------------------
function Write-Head($msg) { Write-Host "`n==== $msg ====" -ForegroundColor Cyan }
function Write-Ok($msg) { Write-Host " PASS $msg" -ForegroundColor Green }
function Write-Fail($msg) { Write-Host " FAIL $msg" -ForegroundColor Red }
function Write-Info($msg) { Write-Host " $msg" -ForegroundColor DarkGray }
# --- environment -----------------------------------------------------------
function Sync-Path {
$m = [Environment]::GetEnvironmentVariable('Path', 'Machine')
$u = [Environment]::GetEnvironmentVariable('Path', 'User')
$env:Path = ($m, $u | Where-Object { $_ }) -join ';'
}
function Resolve-Tool($name, [string[]]$fallbacks) {
$cmd = Get-Command $name -ErrorAction SilentlyContinue
if ($cmd) { return $cmd.Source }
foreach ($f in $fallbacks) { if (Test-Path $f) { return $f } }
throw "$name not found on PATH."
}
function Get-Go { Resolve-Tool 'go.exe' @('C:\Program Files\Go\bin\go.exe') }
function Get-Npm { Resolve-Tool 'npm.cmd' @('C:\Program Files\nodejs\npm.cmd') }
function Get-Npx { Resolve-Tool 'npx.cmd' @('C:\Program Files\nodejs\npx.cmd') }
function Get-Node { Resolve-Tool 'node.exe' @('C:\Program Files\nodejs\node.exe') }
# --- step runner -----------------------------------------------------------
$script:Results = @()
# Run one command, tee its output to a log file, and record pass/fail by exit
# code. Never throws -- a failing step is recorded and the run continues so the
# report covers every layer.
function Invoke-Step {
param(
[string]$Name,
[string]$WorkDir,
[string]$Exe,
[string[]]$CmdArgs,
[string]$LogName
)
Write-Head $Name
$log = Join-Path $LogDir $LogName
Write-Info "log: $log"
$sw = [Diagnostics.Stopwatch]::StartNew()
Push-Location $WorkDir
# A native tool writing to stderr (e.g. a webserver startup warning) must
# not be treated as a PowerShell error. Pass/fail is the exit code alone;
# stderr is just logged. Continue preference keeps 2>&1 from throwing.
$prev = $ErrorActionPreference
$ErrorActionPreference = 'Continue'
try {
& $Exe @CmdArgs 2>&1 | Tee-Object -FilePath $log
$code = $LASTEXITCODE
} finally {
$ErrorActionPreference = $prev
Pop-Location
}
$sw.Stop()
$ok = ($code -eq 0)
$script:Results += [pscustomobject]@{
Layer = $Name; Exit = $code; Ok = $ok; Seconds = [math]::Round($sw.Elapsed.TotalSeconds, 1); Log = $log
}
if ($ok) { Write-Ok "$Name ($($sw.Elapsed.TotalSeconds.ToString('0.0'))s)" }
else { Write-Fail "$Name (exit $code)" }
}
# --- preconditions ---------------------------------------------------------
function Ensure-Inventory {
# Only the browser layers need live, future-dated flight data.
$node = Get-Node
$check = Join-Path $Root 'scripts\db.mjs'
if ($Reseed) {
Write-Info 'Re-seeding database (-Reseed).'
& $node (Join-Path $Root 'scripts\db.mjs') reset | Out-Null
return
}
& $node $check check 2>&1 | Out-Null
if ($LASTEXITCODE -ne 0) {
Write-Info 'Demo inventory is stale or missing -- re-seeding before e2e.'
& $node (Join-Path $Root 'scripts\db.mjs') reset | Out-Null
} else {
Write-Info 'Demo inventory is fresh.'
}
}
# --- layers ----------------------------------------------------------------
function Run-Backend {
$go = Get-Go
$cmdArgs = @('test', './...')
if ($Grep) { $cmdArgs += @('-run', $Grep) }
Invoke-Step 'backend' $Backend $go $cmdArgs 'backend.log'
}
function Run-Api {
# The HTTP contract lives in the handler + middleware packages.
$go = Get-Go
$cmdArgs = @('test', './internal/handlers/...', './internal/middleware/...')
if ($Grep) { $cmdArgs += @('-run', $Grep) }
Invoke-Step 'api' $Backend $go $cmdArgs 'api.log'
}
function Run-Web {
$npx = Get-Npx
Invoke-Step 'web' $Frontend $npx @('ng', 'test', '--no-watch') 'web.log'
}
function Run-Ui {
param([string]$Label = 'ui')
Ensure-Inventory
$npx = Get-Npx
$cmdArgs = @('playwright', 'test')
if ($Grep) { $cmdArgs += @('-g', $Grep) }
Invoke-Step $Label $E2E $npx $cmdArgs "$Label.log"
}
# --- evidence + results collection -----------------------------------------
function Collect-Artifacts {
# JUnit (for CI test publishing) and the evidence report both live under e2e.
$junit = Join-Path $E2E 'results\junit.xml'
if (Test-Path $junit) {
Copy-Item $junit (Join-Path $LogDir 'junit.xml') -Force
Write-Info "JUnit results -> $(Join-Path $LogDir 'junit.xml')"
}
$evidence = Join-Path $E2E 'evidence-report\index.html'
if (Test-Path $evidence) {
Write-Info "Evidence report -> $evidence"
if ($Open) { Start-Process $evidence }
}
}
# --- dispatch --------------------------------------------------------------
Sync-Path
New-Item -ItemType Directory -Force -Path $LogDir | Out-Null
Write-Host "TravelHub test run $RunId (layer: $Layer)" -ForegroundColor White
switch ($Layer) {
'backend' { Run-Backend }
'api' { Run-Api }
'web' { Run-Web }
'ui' { Run-Ui 'ui' }
'binding' { Run-Ui 'binding' }
'unit' { Run-Backend; Run-Web }
'all' { Run-Backend; Run-Web; Run-Ui 'ui' }
}
if (-not $NoReport) { Collect-Artifacts }
# --- summary ---------------------------------------------------------------
Write-Head 'Summary'
$script:Results | Format-Table Layer, @{N='Result';E={ if ($_.Ok) { 'PASS' } else { "FAIL ($($_.Exit))" } }}, Seconds -AutoSize | Out-String | Write-Host
# Write a small machine-readable summary alongside the logs.
$summary = [pscustomobject]@{
runId = $RunId
layer = $Layer
when = (Get-Date).ToString('o')
results = $script:Results
passed = @($script:Results | Where-Object Ok).Count
failed = @($script:Results | Where-Object { -not $_.Ok }).Count
}
$summary | ConvertTo-Json -Depth 4 | Out-File (Join-Path $LogDir 'summary.json') -Encoding utf8
Write-Info "Logs: $LogDir"
$failed = @($script:Results | Where-Object { -not $_.Ok }).Count
if ($failed -gt 0) {
Write-Fail "$failed layer(s) failed."
exit 1
}
Write-Ok 'All layers passed.'
exit 0