#!/usr/bin/env bash # Azaion UI — unit + blackbox test runner. # # Generated by .cursor/skills/test-spec phase 4. Drives the test profiles # specified in _docs/02_document/tests/environment.md: # - static : repo + dist artifact checks (no runtime) # - fast : Bun + Vitest + jsdom + MSW (component / unit / blackbox at the fetch boundary) # - e2e : Playwright (Chromium + Firefox) against the suite docker-compose stack # # The fast + static profiles run locally on host. The e2e profile delegates to the # suite-level docker-compose harness owned by the parent suite repo (e2e/docker-compose.suite-e2e.yml). # # Hardware-Dependency Assessment recorded "Not hardware-dependent" — Docker is preferred # for e2e; fast + static execute on the host because they have no runtime dependency on the suite. # # Usage: # scripts/run-tests.sh # static + fast (default; gates every commit per CI/CD Integration) # scripts/run-tests.sh --unit-only # alias for default — fast + static, no e2e # scripts/run-tests.sh --all # static + fast + e2e # scripts/run-tests.sh --e2e-only # only the e2e profile # scripts/run-tests.sh --static-only # only the static checks set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" SUITE_ROOT="$(cd "$PROJECT_ROOT/.." && pwd)" RESULTS_DIR="$PROJECT_ROOT/test-results" RUN_STATIC=true RUN_FAST=true RUN_E2E=false for arg in "$@"; do case "$arg" in --unit-only) RUN_STATIC=true; RUN_FAST=true; RUN_E2E=false ;; --all) RUN_STATIC=true; RUN_FAST=true; RUN_E2E=true ;; --e2e-only) RUN_STATIC=false; RUN_FAST=false; RUN_E2E=true ;; --static-only) RUN_STATIC=true; RUN_FAST=false; RUN_E2E=false ;; --fast-only) RUN_STATIC=false; RUN_FAST=true; RUN_E2E=false ;; -h|--help) sed -n '2,18p' "$0" | sed 's/^# \{0,1\}//' exit 0 ;; *) echo "Unknown argument: $arg" >&2 echo "Run with --help for usage." >&2 exit 2 ;; esac done E2E_COMPOSE_STARTED_HERE=false cleanup() { if [ "$E2E_COMPOSE_STARTED_HERE" = "true" ]; then docker compose -f "$SUITE_ROOT/e2e/docker-compose.suite-e2e.yml" down -v --remove-orphans || true fi } trap cleanup EXIT mkdir -p "$RESULTS_DIR" cd "$PROJECT_ROOT" echo "[run-tests] project root: $PROJECT_ROOT" echo "[run-tests] suite root : $SUITE_ROOT" echo "[run-tests] profiles : static=$RUN_STATIC fast=$RUN_FAST e2e=$RUN_E2E" # ---------------------------------------------------------------------------- # Install dependencies (mandatory — a fresh CI runner has nothing). # ---------------------------------------------------------------------------- if [ "$RUN_FAST" = "true" ] || [ "$RUN_STATIC" = "true" ]; then if ! command -v bun >/dev/null 2>&1; then echo "[run-tests] FATAL: bun is required (project pins bun@1.3.11 per package.json packageManager)." >&2 exit 1 fi echo "[run-tests] installing dependencies (bun install --frozen-lockfile if lockfile present, else bun install)..." if [ -f "$PROJECT_ROOT/bun.lock" ] || [ -f "$PROJECT_ROOT/bun.lockb" ]; then bun install --frozen-lockfile else bun install fi fi OVERALL_EXIT=0 # ---------------------------------------------------------------------------- # Static profile — repo + dist artifact checks. # Source: _docs/02_document/tests/blackbox-tests.md, security-tests.md, # resource-limit-tests.md, traceability-matrix.md "STC-*" candidates. # # Today only the spec-derived checks ship; the STC-S* family lands when the # traceability matrix promotes them (see Phase 3 "Still open" item 6). # ---------------------------------------------------------------------------- if [ "$RUN_STATIC" = "true" ]; then echo "[run-tests] === static profile ===" STATIC_REPORT="$RESULTS_DIR/static-report.txt" : > "$STATIC_REPORT" STATIC_FAIL=0 echo "[static] STC-S1: TypeScript strict mode in tsconfig.json" if node -e 'const t=require("./tsconfig.json"); process.exit((t.compilerOptions && t.compilerOptions.strict === true) ? 0 : 1)' 2>/dev/null; then echo " PASS" | tee -a "$STATIC_REPORT" else # tsconfig may extend a base; fall back to a tsc --showConfig dry-run. if bunx tsc --showConfig | grep -q '"strict": true'; then echo " PASS (via tsc --showConfig)" | tee -a "$STATIC_REPORT" else echo " FAIL: strict mode not enabled" | tee -a "$STATIC_REPORT"; STATIC_FAIL=1 fi fi echo "[static] STC-S2..S11: pinned dependency versions (S2 React 19, S3 Vite 6, S4 Bun 1.3.11, S7 no Redux/Zustand/TanStack, S8 Tailwind 4, S9 Leaflet, S10 Chart.js, S11 DnD)" node -e ' const p = require("./package.json"); const all = Object.assign({}, p.dependencies || {}, p.devDependencies || {}); const pin = (name, ver) => (all[name] || "").startsWith(ver) ? ` PASS ${name}@${all[name]}` : ` FAIL ${name}@${all[name] || "(missing)"} expected ${ver}*`; const ban = (name) => all[name] ? ` FAIL banned dep present: ${name}` : ` PASS no ${name}`; const lines = [ pin("react", "^19"), pin("react-dom", "^19"), pin("vite", "^6"), pin("tailwindcss", "^4"), pin("leaflet", "^1.9.4"), pin("react-leaflet", "^5"), pin("chart.js", "^4"), pin("@hello-pangea/dnd", "^18"), ban("redux"), ban("@reduxjs/toolkit"), ban("zustand"), ban("@tanstack/react-query"), ban("@tanstack/query-core"), (p.packageManager === "bun@1.3.11") ? " PASS packageManager bun@1.3.11" : ` FAIL packageManager=${p.packageManager}`, ]; for (const l of lines) console.log(l); if (lines.some(l => l.startsWith(" FAIL"))) process.exit(1); ' | tee -a "$STATIC_REPORT" || STATIC_FAIL=1 echo "[static] STC-N2 / AC-N2: no in-browser ML libraries" if node -e ' const p = require("./package.json"); const all = Object.assign({}, p.dependencies || {}, p.devDependencies || {}); const re = /(onnxruntime|tensorflow|tflite|coreml|tfjs|@tensorflow\/|@huggingface\/|transformers\.js)/i; const hits = Object.keys(all).filter(n => re.test(n)); if (hits.length) { console.log(" FAIL banned ML deps:", hits.join(", ")); process.exit(1); } console.log(" PASS no in-browser ML deps"); ' | tee -a "$STATIC_REPORT"; then :; else STATIC_FAIL=1; fi echo "[static] STC-N4 / AC-N4: no response-signature library" if node -e ' const p = require("./package.json"); const all = Object.assign({}, p.dependencies || {}, p.devDependencies || {}); const re = /(jsrsasign|tweetnacl|@noble\/|^jose$)/i; const hits = Object.keys(all).filter(n => re.test(n)); if (hits.length) { console.log(" FAIL signature libs:", hits.join(", ")); process.exit(1); } console.log(" PASS no signature libs"); ' | tee -a "$STATIC_REPORT"; then :; else STATIC_FAIL=1; fi echo "[static] STC-S13 / O2: no client-side persistence library" if node -e ' const p = require("./package.json"); const all = Object.assign({}, p.dependencies || {}, p.devDependencies || {}); const re = /^(localforage|idb|dexie)$/i; const hits = Object.keys(all).filter(n => re.test(n)); if (hits.length) { console.log(" FAIL persistence libs:", hits.join(", ")); process.exit(1); } console.log(" PASS no persistence libs"); ' | tee -a "$STATIC_REPORT"; then :; else STATIC_FAIL=1; fi echo "[static] STC-S6 / O11: no WebSocket / GraphQL / gRPC-Web / SSR / RSC" if node -e ' const p = require("./package.json"); const all = Object.assign({}, p.dependencies || {}, p.devDependencies || {}); const re = /^(ws|socket\.io|graphql|apollo|@apollo\/|grpc-web|react-dom\/server)$/i; const hits = Object.keys(all).filter(n => re.test(n)); if (hits.length) { console.log(" FAIL banned deps:", hits.join(", ")); process.exit(1); } console.log(" PASS no WS/GraphQL/gRPC/SSR deps"); ' | tee -a "$STATIC_REPORT"; then :; else STATIC_FAIL=1; fi echo "[static] AC-N5: dropped legacy features (SoundDetections, DroneMaintenance) absent from src/ + mission-planner/" if grep -r --include='*.ts' --include='*.tsx' --include='*.js' --include='*.jsx' -E 'SoundDetections|DroneMaintenance' "$PROJECT_ROOT/src" "$PROJECT_ROOT/mission-planner" 2>/dev/null | tee -a "$STATIC_REPORT"; then echo " FAIL legacy symbols present" | tee -a "$STATIC_REPORT"; STATIC_FAIL=1 else echo " PASS no legacy symbols" | tee -a "$STATIC_REPORT" fi echo "[static] AC-31 / O12: mission-planner not built into dist/" if [ -d "$PROJECT_ROOT/dist" ]; then if grep -rE 'mission[-_ ]?planner' "$PROJECT_ROOT/dist" 2>/dev/null | tee -a "$STATIC_REPORT"; then echo " FAIL mission-planner symbols leaked into dist/" | tee -a "$STATIC_REPORT"; STATIC_FAIL=1 else echo " PASS mission-planner absent from dist/" | tee -a "$STATIC_REPORT" fi else echo " SKIP dist/ not built — re-run after 'bun run build'" | tee -a "$STATIC_REPORT" fi echo "[static] AC-N3: no service worker registration" if grep -rE 'serviceWorker\.register|navigator\.serviceWorker' "$PROJECT_ROOT/src" 2>/dev/null | tee -a "$STATIC_REPORT"; then echo " FAIL service worker registration found" | tee -a "$STATIC_REPORT"; STATIC_FAIL=1 else echo " PASS no service worker registration" | tee -a "$STATIC_REPORT" fi echo "[static] NFT-SEC-09 source check (quarantined until Step 4): OpenWeatherMap key not in source" if grep -rE 'OPENWEATHERMAP|OWM_API_KEY|appid=' "$PROJECT_ROOT/src" 2>/dev/null | grep -vE 'import\.meta\.env|process\.env' | tee -a "$STATIC_REPORT"; then echo " QUARANTINED FAIL: literal OWM key string found (Step 4 will fix)" | tee -a "$STATIC_REPORT" # Quarantined per traceability-matrix.md — do not gate on this until Step 4. else echo " PASS no literal OWM key" | tee -a "$STATIC_REPORT" fi if [ "$STATIC_FAIL" = "1" ]; then echo "[run-tests] static profile FAILED — see $STATIC_REPORT" OVERALL_EXIT=1 else echo "[run-tests] static profile PASSED" fi fi # ---------------------------------------------------------------------------- # Fast profile — Bun + Vitest + jsdom + MSW. # Implementation of *.test.ts(x) files lands at autodev Step 5 (Decompose Tests); # this runner block is the harness the decomposed tasks plug into. # ---------------------------------------------------------------------------- if [ "$RUN_FAST" = "true" ]; then echo "[run-tests] === fast profile ===" FAST_REPORT="$RESULTS_DIR/fast-report.txt" # Vitest is the planned fast-profile runner (decided at decompose time). If # the test runner has not been wired into package.json yet, fail loudly so # the decomposer sees the gap rather than silently passing. if grep -q '"test"' "$PROJECT_ROOT/package.json" 2>/dev/null; then echo "[fast] running bun run test" if bun run test 2>&1 | tee "$FAST_REPORT"; then echo "[run-tests] fast profile PASSED" else echo "[run-tests] fast profile FAILED — see $FAST_REPORT" OVERALL_EXIT=1 fi else echo "[fast] no \"test\" script in package.json yet — decompose-tests step (autodev Step 5) wires the runner." echo "[fast] SKIPPED (no runner)" | tee "$FAST_REPORT" # Do not gate; this is the expected state before Step 5 ships. fi fi # ---------------------------------------------------------------------------- # E2E profile — Playwright (Chromium + Firefox) against the suite docker stack. # The compose file is owned by the parent suite repo; this script only invokes it. # ---------------------------------------------------------------------------- if [ "$RUN_E2E" = "true" ]; then echo "[run-tests] === e2e profile ===" COMPOSE_FILE="$SUITE_ROOT/e2e/docker-compose.suite-e2e.yml" E2E_REPORT="$RESULTS_DIR/e2e-report.txt" if [ ! -f "$COMPOSE_FILE" ]; then echo "[e2e] FATAL: $COMPOSE_FILE not found." >&2 echo "[e2e] The suite-level docker-compose harness is owned by the parent suite repo (..)." >&2 echo "[e2e] See _docs/02_document/tests/environment.md → Test Execution → Docker mode." >&2 OVERALL_EXIT=1 elif ! command -v docker >/dev/null 2>&1; then echo "[e2e] FATAL: docker is required for the e2e profile." >&2 OVERALL_EXIT=1 else echo "[e2e] starting compose stack at $COMPOSE_FILE..." docker compose -f "$COMPOSE_FILE" up -d --build E2E_COMPOSE_STARTED_HERE=true echo "[e2e] running playwright-runner..." if docker compose -f "$COMPOSE_FILE" run --rm playwright-runner 2>&1 | tee "$E2E_REPORT"; then echo "[run-tests] e2e profile PASSED" else echo "[run-tests] e2e profile FAILED — see $E2E_REPORT" OVERALL_EXIT=1 fi fi fi # ---------------------------------------------------------------------------- # Summary # ---------------------------------------------------------------------------- echo "" echo "[run-tests] summary" echo "[run-tests] static profile : $([ "$RUN_STATIC" = "true" ] && echo "ran" || echo "skipped")" echo "[run-tests] fast profile : $([ "$RUN_FAST" = "true" ] && echo "ran" || echo "skipped")" echo "[run-tests] e2e profile : $([ "$RUN_E2E" = "true" ] && echo "ran" || echo "skipped")" echo "[run-tests] results dir : $RESULTS_DIR" echo "[run-tests] exit code : $OVERALL_EXIT" exit "$OVERALL_EXIT"