Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Lx9FNLG2CqeBbVGGd9bFsP
152 lines
7.9 KiB
Bash
Executable File
152 lines
7.9 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Gate M3: a port's in/out graph renders at 1 h, 24 h and 30 d, and a 48-port page renders in under 2 s.
|
|
# Procedure: docs/milestones/M3-graphs-and-ui.md.
|
|
# 1. make lint; unit tests of the UI server, the OpenAPI differ and the dev tools.
|
|
# 2. make build: the UI is built and embedded in the single static binary, which does not contain the dev tools.
|
|
# 3. nmon serve on a temp data dir (--poll-interval 10s) with `nmon-devtools seed-demo` (make devtools, a separate
|
|
# binary that is never shipped): simulated devices (the counters fixture, a 48-port switch, routers, a UPS, a
|
|
# 32-bit device) are added through the API and 31 days of synthetic samples are back-filled into hot; wait for
|
|
# real polls on top.
|
|
# 4. scripts/openapi-check.sh: no incompatible change since the last m*/v* tag, and openapi.json is current.
|
|
# 5. Contract drift: regenerate the web client from the live /openapi.json; npm run build must type-check.
|
|
# 6. Playwright against the real server: device page -> port graph -> 1 h -> 24 h -> 30 d with non-empty in and
|
|
# out series each; the 48-graph page under 2 s. Then live screenshots of every page into docs/screenshots/M3/.
|
|
# Needs: go, make, node 22+, npm, Playwright's chromium (cd web && npx playwright install chromium), curl, awk.
|
|
set -uo pipefail
|
|
|
|
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
cd "$ROOT"
|
|
|
|
# Maintainability is a hard rule (docs/MAINTAINABILITY.md): no gate passes with lint findings.
|
|
make lint || { echo "FAIL: make lint"; exit 1; }
|
|
|
|
TMP="$(mktemp -d)"
|
|
FAILS=0
|
|
NMON_PID=""
|
|
SEED_PID=""
|
|
SHOTS="$ROOT/${NMON_SHOT_DIR:-docs/screenshots/M3}"
|
|
|
|
pass() { echo "PASS: $*"; }
|
|
fail() { echo "FAIL: $*"; FAILS=$((FAILS + 1)); }
|
|
die() { fail "$*"; exit 1; }
|
|
|
|
cleanup() {
|
|
[ -n "$SEED_PID" ] && kill "$SEED_PID" 2>/dev/null && wait "$SEED_PID" 2>/dev/null
|
|
if [ -n "$NMON_PID" ]; then kill -TERM "$NMON_PID" 2>/dev/null; wait "$NMON_PID" 2>/dev/null; fi
|
|
rm -rf "$TMP"
|
|
}
|
|
trap cleanup EXIT
|
|
trap 'exit 130' INT TERM
|
|
|
|
pick_port() { # a port free for both TCP and UDP
|
|
local p
|
|
while :; do
|
|
p=$((20000 + RANDOM % 30000))
|
|
ss -H -lntu 2>/dev/null | awk '{print $5}' | grep -q "[:.]$p\$" || { echo "$p"; return; }
|
|
done
|
|
}
|
|
|
|
# wait_for <seconds> <description> <command...>
|
|
wait_for() {
|
|
local n="$1" what="$2"; shift 2
|
|
for _ in $(seq 1 "$((n * 2))"); do "$@" && return 0; sleep 0.5; done
|
|
echo "timed out after ${n}s waiting for $what"; return 1
|
|
}
|
|
|
|
# 1. Unit tests of what this milestone adds on the Go side.
|
|
echo "== unit tests"
|
|
if go test -race -count=1 ./internal/webui/... ./internal/openapidiff/... ./cmd/nmon-devtools/... ./cmd/nmon/... >"$TMP/unit.log" 2>&1; then
|
|
pass "go test -race: webui, openapidiff, nmon-devtools, nmon"
|
|
else
|
|
tail -n 30 "$TMP/unit.log"; fail "go test -race"
|
|
fi
|
|
|
|
# 2. One static binary with the UI inside, and no dev tools.
|
|
echo "== make build (UI embedded)"
|
|
make build >"$TMP/build.log" 2>&1 || { tail -n 30 "$TMP/build.log"; die "make build"; }
|
|
strings -a ./nmon | grep -q 'seed-demo' && fail "the shipped nmon contains the dev tools" || pass "the shipped nmon does not contain the dev tools"
|
|
make devtools >"$TMP/devtools.log" 2>&1 || { tail -n 30 "$TMP/devtools.log"; die "make devtools"; }
|
|
file ./nmon | grep -q 'statically linked' && pass "nmon is a static binary" || fail "nmon is not statically linked"
|
|
ls internal/webui/dist/assets/*.js >/dev/null 2>&1 && pass "UI build embedded: $(ls internal/webui/dist/assets | wc -l) asset files" ||
|
|
fail "no UI build in internal/webui/dist (make web)"
|
|
|
|
# 3. Server + seed.
|
|
echo "== nmon serve + seed-demo"
|
|
PORT="$(pick_port)"
|
|
URL="http://127.0.0.1:$PORT"
|
|
DATA="$TMP/data"
|
|
./nmon serve --data-dir "$DATA" --secret-key-file "$TMP/secret.key" --insecure-http --listen "127.0.0.1:$PORT" \
|
|
--poll-interval 10s --trap-listen '' >"$TMP/nmon.log" 2>&1 &
|
|
NMON_PID=$!
|
|
up() { curl -fsS "$URL/api/v1/devices" >/dev/null 2>&1; }
|
|
wait_for 60 "nmon to serve" up || { tail -n 20 "$TMP/nmon.log"; die "nmon serve did not start"; }
|
|
HOT="$(grep -o 'hot=http://[^ ]*' "$TMP/nmon.log" | head -n 1 | cut -d= -f2)"
|
|
[ -n "$HOT" ] || die "no hot store URL in the serve log"
|
|
pass "nmon serving at $URL (hot $HOT)"
|
|
|
|
curl -fsS "$URL/" | grep -q 'id="root"' && pass "GET / serves the embedded SPA" || fail "GET / does not serve the SPA"
|
|
curl -fsS "$URL/devices/anything" | grep -q 'id="root"' && pass "client routes fall back to index.html" || fail "history fallback"
|
|
[ "$(curl -s -o /dev/null -w '%{http_code}' "$URL/api/v1/nope")" = 404 ] && pass "unknown /api paths do not fall back to the SPA" || fail "/api/v1/nope"
|
|
curl -fsSI "$URL/" | grep -qi '^cache-control: no-cache' && pass "index.html is no-cache" || fail "index.html cache-control"
|
|
ASSET="$(curl -fsS "$URL/" | grep -o '/assets/[^"]*\.js' | head -n 1)"
|
|
curl -fsSI "$URL$ASSET" | grep -qi 'immutable' && pass "hashed assets are immutable" || fail "asset cache-control"
|
|
|
|
./nmon-devtools seed-demo --api "$URL" --hot-url "$HOT" --env-out "$TMP/env" >"$TMP/seed.log" 2>&1 &
|
|
SEED_PID=$!
|
|
seeded() { grep -q 'seed-demo ready' "$TMP/seed.log"; }
|
|
wait_for 480 "seed-demo (devices added, 31 days back-filled)" seeded || { tail -n 30 "$TMP/seed.log"; die "seed-demo"; }
|
|
sed -n 's/^/ /p' "$TMP/seed.log"
|
|
set -a; . "$TMP/env"; set +a
|
|
[ -n "${NMON_DEVICE:-}" ] && [ -n "${NMON_DEVICE_48:-}" ] || die "seed-demo did not report the counters and 48-port devices"
|
|
pass "seeded: counters device $NMON_DEVICE, 48-port device $NMON_DEVICE_48"
|
|
|
|
# Real polls on top of the history: every simulated device answers and keeps being polled.
|
|
polled() { # two polls after the back-fill: nmon_snmp_up present for every added device
|
|
local n; n="$(curl -fsS -G "$HOT/api/v1/query" --data-urlencode 'query=count(nmon_snmp_up == 1)' | grep -o '"value":\[[^]]*\]' | sed 's/.*,"\([0-9]*\)".*/\1/')"
|
|
[ "${n:-0}" -ge 8 ]
|
|
}
|
|
wait_for 90 "devices to answer polls" polled && pass "live polls running on top of the back-filled history" || fail "live polls"
|
|
sleep 25
|
|
|
|
# 4. OpenAPI compatibility.
|
|
echo "== openapi"
|
|
curl -fsS "$URL/openapi.json" >"$TMP/openapi.live.json" || die "GET /openapi.json"
|
|
OPENAPI_NEW="$TMP/openapi.live.json" ./scripts/openapi-check.sh && pass "openapi-check" || fail "openapi-check"
|
|
if ./nmon openapi >"$TMP/openapi.cli.json" 2>/dev/null; then
|
|
cmp -s "$TMP/openapi.cli.json" openapi.json && pass "openapi.json matches nmon openapi" || fail "openapi.json is stale: run ./nmon openapi > openapi.json"
|
|
fi
|
|
|
|
# 5. Contract drift: the client generated from the live spec must still compile.
|
|
echo "== contract drift"
|
|
cp web/src/api/schema.d.ts "$TMP/schema.before.d.ts"
|
|
if (cd web && npm run gen:api -- "$URL/openapi.json" >"$TMP/genapi.log" 2>&1 && npm run build >"$TMP/webbuild.log" 2>&1); then
|
|
pass "web client generated from the live spec type-checks and builds"
|
|
else
|
|
tail -n 30 "$TMP/genapi.log" "$TMP/webbuild.log" 2>/dev/null; fail "web build against the live spec"
|
|
fi
|
|
cmp -s web/src/api/schema.d.ts "$TMP/schema.before.d.ts" && pass "web/src/api/schema.d.ts is current" ||
|
|
echo "NOTE: web/src/api/schema.d.ts changed after regeneration from the live spec; review and commit it"
|
|
(cd web && npm run lint >"$TMP/weblint.log" 2>&1) && pass "web lint after regeneration" || { tail -n 20 "$TMP/weblint.log"; fail "web lint after regeneration"; }
|
|
|
|
# 6. Browser journey and screenshots against the real server.
|
|
echo "== playwright (live)"
|
|
export NMON_URL="$URL"
|
|
if (cd web && npm run e2e:live -- gate.spec.ts) >"$TMP/e2e.log" 2>&1; then
|
|
pass "port graph renders in and out at 1 h, 24 h, 30 d; 48-port page under 2 s"
|
|
else
|
|
tail -n 40 "$TMP/e2e.log"; fail "playwright gate journey"
|
|
fi
|
|
sed -n 's/^/ /p' "$TMP/e2e.log" | grep -E "✓|✘|passed|failed|ok " | head -n 10
|
|
|
|
echo "== screenshots"
|
|
rm -rf "$SHOTS"; mkdir -p "$SHOTS"
|
|
if (cd web && NMON_SHOT_DIR="$SHOTS" npm run e2e:live -- screenshots.spec.ts) >"$TMP/shots.log" 2>&1; then
|
|
N="$(ls "$SHOTS"/*.png 2>/dev/null | wc -l)"
|
|
[ "$N" -ge 15 ] && pass "$N screenshots in $SHOTS" || fail "only $N screenshots in $SHOTS"
|
|
ls "$SHOTS"
|
|
else
|
|
tail -n 40 "$TMP/shots.log"; fail "screenshots"
|
|
fi
|
|
|
|
if [ "$FAILS" -eq 0 ]; then echo "GATE M3: PASS"; else echo "GATE M3: FAIL ($FAILS)"; exit 1; fi
|