mirror of
https://github.com/Kpa-clawbot/meshcore-analyzer.git
synced 2026-04-25 15:52:08 +00:00
Adds the CoreScope-side artifacts that pair with the generic [`qa-suite` skill](https://github.com/Kpa-clawbot/ai-sdlc/pull/1). ## Layout ``` qa/ ├── README.md ├── plans/ │ └── v3.6.0-rc.md # 34-commit test plan since v3.5.1 └── scripts/ └── api-contract-diff.sh # CoreScope-tuned API contract diff ``` The skill ships the reusable engine + qa-engineer persona + an example plan. This PR adds the CoreScope-tuned plan and the CoreScope-tuned script (correct seed lookups for our `{packets, total}` response shape, our endpoint list, our `resolved_path` requirement). Read by the parent agent at runtime. ## How to use From chat: - `qa staging` — runs the latest `qa/plans/v*-rc.md` against staging, files a fresh GH issue with the report - `qa pr <N>` — uses `qa/plans/pr-<N>.md` if present, else latest RC plan; comments on the PR - `qa v3.6.0-rc` — runs that specific plan The qa-engineer subagent walks every step, classifying each as `auto` (script) / `browser` (UI assertion) / `human` (manual) / `browser+auto`. Quantified pass criteria are mandatory — banned phrases: 'visually aligned' / 'fast' / 'no regression'. ## Plan v3.6.0-rc contents Covers the 34 commits since v3.5.1: - §1 Memory & Load (#806, #790, #807) — heap thresholds, sawtooth pattern - §2 API contract (#806) — every endpoint that should carry `resolved_path`, auto-checked by `api-contract-diff.sh` - §3 Decoder & hashing (#787, #732, #747, #766, #794, #761) - §4 Channels (#725 series M1–M5) - §5 Clock skew (#690 series M1–M3) - §6 Observers (#764, #774) - §7 Multi-byte hash adopters (#758, #767) - §8 Frontend nav & deep linking (#739, #740, #779, #785, #776, #745) - §9 Geofilter (#735, #734) - §10 Node blacklist (#742) - §11 Deploy/ops Release blockers: §1.2, §2, §3. §4 is the headline-feature gate. ## Adding new plans Per release: copy `plans/v<last>-rc.md` to `plans/v<new>-rc.md` and update commit-range header, new sections, GO criteria. Per PR: create `plans/pr-<N>.md` with the bare minimum for that PR's surface area. Co-authored-by: you <you@example.com>
135 lines
5.8 KiB
Bash
Executable File
135 lines
5.8 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# api-contract-diff.sh — diff CoreScope API endpoints between two deployments.
|
|
# Usage: api-contract-diff.sh BASELINE_URL TARGET_URL [-k AUTH_HEADER]
|
|
#
|
|
# Compares JSON shape (recursive key set) per endpoint and asserts presence of
|
|
# `resolved_path` where contract requires it. Prints a per-endpoint result line
|
|
# (✅/❌) and a summary. Exit code = number of failures.
|
|
#
|
|
# Distinguishes:
|
|
# curl-failed → HTTP error or network timeout (real outage)
|
|
# parse-empty → curl succeeded but response shape unexpected (probable
|
|
# contract drift in this script or in the API)
|
|
# shape-diff → recursive key set differs between baseline and target
|
|
# rp-missing → resolved_path absent on target where it was promised
|
|
#
|
|
# PUBLIC repo: do not commit URLs or keys here. Caller passes them.
|
|
|
|
set -uo pipefail
|
|
|
|
OLD="${1:-}"; NEW="${2:-}"
|
|
[[ -z "$OLD" || -z "$NEW" ]] && { echo "usage: $0 BASELINE_URL TARGET_URL [-k AUTH_HEADER]" >&2; exit 2; }
|
|
shift 2 || true
|
|
AUTH=""
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
-k) AUTH="$2"; shift 2 ;;
|
|
*) echo "unknown arg: $1" >&2; exit 2 ;;
|
|
esac
|
|
done
|
|
|
|
TMP=$(mktemp -d); trap 'rm -rf "$TMP"' EXIT
|
|
|
|
# Wrapper: fetch URL, return body on stdout, exit 1 on HTTP error / timeout.
|
|
fetch() {
|
|
local url="$1" out="$2"
|
|
local code
|
|
code=$(curl -s -m 30 -o "$out" -w "%{http_code}" ${AUTH:+-H "$AUTH"} "$url" 2>/dev/null) || code="000"
|
|
if [[ "$code" != "2"* ]]; then
|
|
echo " HTTP $code"
|
|
return 1
|
|
fi
|
|
return 0
|
|
}
|
|
|
|
# Seed lookups from TARGET (so the picked IDs are guaranteed present there).
|
|
seed_packets="$TMP/seed_packets.json"
|
|
seed_observers="$TMP/seed_observers.json"
|
|
seed_nodes="$TMP/seed_nodes.json"
|
|
|
|
if ! fetch "$NEW/api/packets?limit=1" "$seed_packets"; then echo "seed /api/packets failed" >&2; fi
|
|
if ! fetch "$NEW/api/observers" "$seed_observers"; then echo "seed /api/observers failed" >&2; fi
|
|
if ! fetch "$NEW/api/nodes?limit=1" "$seed_nodes"; then echo "seed /api/nodes failed" >&2; fi
|
|
|
|
HASH=$(jq -r '.packets[0].hash // empty' "$seed_packets" 2>/dev/null || true)
|
|
OBSID=$(jq -r '.observers[0].id // empty' "$seed_observers" 2>/dev/null || true)
|
|
NODEPK=$(jq -r '.nodes[0].public_key // empty' "$seed_nodes" 2>/dev/null || true)
|
|
|
|
[[ -z "$HASH" ]] && echo "warn: no packet hash from /api/packets — packet-detail endpoints will be skipped" >&2
|
|
[[ -z "$OBSID" ]] && echo "warn: no observer id from /api/observers — observer-detail endpoints will be skipped" >&2
|
|
[[ -z "$NODEPK" ]] && echo "warn: no node pubkey from /api/nodes — node-detail endpoints will be skipped" >&2
|
|
|
|
# Endpoints to diff: path | jq filter (selects subobject to compare) | RP-required(yes/no)
|
|
declare -a ENDPOINTS
|
|
ENDPOINTS+=("/api/packets?limit=20|.packets[0]|yes")
|
|
ENDPOINTS+=("/api/packets?limit=20&expandObservations=true|.packets[0]|yes")
|
|
ENDPOINTS+=("/api/observers|.observers[0]|no")
|
|
[[ -n "$HASH" ]] && ENDPOINTS+=("/api/packets/$HASH|.|yes")
|
|
[[ -n "$OBSID" ]] && ENDPOINTS+=("/api/observers/$OBSID|.|no")
|
|
[[ -n "$OBSID" ]] && ENDPOINTS+=("/api/observers/$OBSID/analytics|.|no")
|
|
[[ -n "$NODEPK" ]] && ENDPOINTS+=("/api/nodes/$NODEPK/health|.recentPackets[0]|yes")
|
|
[[ -n "$NODEPK" ]] && ENDPOINTS+=("/api/nodes/$NODEPK/paths|.|no")
|
|
|
|
# Strip volatile fields (timestamps + counters) from a JSON value.
|
|
STRIP='walk(if type=="object" then del(.timestamp, .first_seen, .last_seen, .last_heard, .updated_at, .server_time, .packet_count, .packetsLastHour, .uptime_secs, .battery_mv, .noise_floor, .observation_count, .advert_count) else . end)'
|
|
|
|
fails=0
|
|
for ep in "${ENDPOINTS[@]}"; do
|
|
IFS='|' read -r path filter need_rp <<<"$ep"
|
|
echo "=== $path (resolved_path required: $need_rp) ==="
|
|
|
|
oldfile="$TMP/old.json"; newfile="$TMP/new.json"
|
|
if ! fetch "$OLD$path" "$oldfile"; then echo " ❌ baseline curl-failed"; fails=$((fails+1)); continue; fi
|
|
if ! fetch "$NEW$path" "$newfile"; then echo " ❌ target curl-failed"; fails=$((fails+1)); continue; fi
|
|
|
|
# Selector + strip on each side. jq stderr is preserved so script bugs surface.
|
|
oldj=$(jq "$filter | $STRIP" "$oldfile")
|
|
jq_old_rc=$?
|
|
newj=$(jq "$filter | $STRIP" "$newfile")
|
|
jq_new_rc=$?
|
|
|
|
if [[ $jq_old_rc -ne 0 ]]; then
|
|
echo " ❌ baseline jq-error (filter='$filter') — likely script bug or API shape changed"
|
|
fails=$((fails+1)); continue
|
|
fi
|
|
if [[ $jq_new_rc -ne 0 ]]; then
|
|
echo " ❌ target jq-error (filter='$filter') — likely script bug or API shape changed"
|
|
fails=$((fails+1)); continue
|
|
fi
|
|
if [[ -z "$oldj" || "$oldj" == "null" ]]; then
|
|
echo " ❌ baseline parse-empty (filter returned empty/null; check API shape)"
|
|
fails=$((fails+1)); continue
|
|
fi
|
|
if [[ -z "$newj" || "$newj" == "null" ]]; then
|
|
echo " ❌ target parse-empty (filter returned empty/null; check API shape)"
|
|
fails=$((fails+1)); continue
|
|
fi
|
|
|
|
# Recursive key-set diff. Canonicalize array indices (numbers) → "[]" so two
|
|
# different sample responses with different array lengths don't false-positive.
|
|
KEYS_FILTER='[paths(scalars or type=="null" or (type=="array" and length==0) or (type=="object" and length==0)) | map(if type=="number" then "[]" else . end) | join(".")] | unique | .[]'
|
|
oldkeys=$(echo "$oldj" | jq -r "$KEYS_FILTER" | sort -u)
|
|
newkeys=$(echo "$newj" | jq -r "$KEYS_FILTER" | sort -u)
|
|
if ! diff <(echo "$oldkeys") <(echo "$newkeys") >/dev/null; then
|
|
echo " ❌ shape-diff (key set differs):"
|
|
diff <(echo "$oldkeys") <(echo "$newkeys") | sed 's/^/ /'
|
|
fails=$((fails+1))
|
|
continue
|
|
fi
|
|
|
|
# If RP expected, assert present on target (any value, may be null).
|
|
if [[ "$need_rp" == "yes" ]]; then
|
|
if ! echo "$newj" | jq -e '.. | objects | select(has("resolved_path")) | .resolved_path' >/dev/null 2>&1; then
|
|
echo " ❌ rp-missing (resolved_path not present anywhere in selector)"
|
|
fails=$((fails+1))
|
|
continue
|
|
fi
|
|
fi
|
|
|
|
echo " ✅ ok"
|
|
done
|
|
|
|
echo
|
|
echo "failures: $fails / ${#ENDPOINTS[@]}"
|
|
exit $fails
|