swactor/examples/pipeline-parallel-inference/scripts/assert-bundle.sh

162 lines
6.3 KiB
Bash
Raw Normal View History

2026-05-20 07:41:30 +00:00
#!/usr/bin/env bash
# assert-bundle.sh — bundle invariant checks for the diagnostics e2e gate.
#
# Given a finalized tarball produced by `swactor-diag-collector`, runs
# the post-processor against it and verifies the structural properties
# the S12 plan requires:
#
# 1. The manifest lists the orchestrator and every stage.
# 2. Each node has at least one snapshot.
# 3. Events from each tier (1: SWIM/dial/message; 2: iroh-internal;
# 3: probe/host/process) appear somewhere in the bundle.
# 4. `swactor-diag-postproc` exits 0.
# 5. The produced `summary.md` is non-empty.
#
# Usage:
# assert-bundle.sh <bundle.tar.gz> <expected_stage_count> [<postproc-binary>]
#
# Exits 0 on success, prints a diagnostic to stderr and exits 1 on any
# failure. Designed to be called from `docker-diag-e2e.sh` but usable
# standalone for ad-hoc verification of a captured bundle.
set -euo pipefail
BUNDLE="${1:?usage: assert-bundle.sh <bundle.tar.gz> <expected_stages> [postproc]}"
EXPECTED_STAGES="${2:?usage: assert-bundle.sh <bundle.tar.gz> <expected_stages> [postproc]}"
POSTPROC_BIN="${3:-swactor-diag-postproc}"
if [ ! -s "$BUNDLE" ]; then
echo "assert-bundle: $BUNDLE does not exist or is empty" >&2
exit 1
fi
WORKDIR="$(mktemp -d -t pp-diag-assert.XXXXXX)"
trap 'rm -rf "$WORKDIR"' EXIT
EXTRACTED="$WORKDIR/extracted"
mkdir -p "$EXTRACTED"
tar -xzf "$BUNDLE" -C "$EXTRACTED"
# The bundle's top-level dir is named after the run id. We don't need to
# know the name in advance — walk one level down.
RUN_DIR=$(find "$EXTRACTED" -mindepth 1 -maxdepth 1 -type d | head -n 1)
if [ -z "$RUN_DIR" ]; then
echo "assert-bundle: bundle has no top-level run directory" >&2
ls -lR "$EXTRACTED" >&2
exit 1
fi
echo "assert-bundle: run dir $RUN_DIR"
# (1) Manifest sanity.
MANIFEST="$RUN_DIR/MANIFEST.json"
if [ ! -s "$MANIFEST" ]; then
echo "assert-bundle: MANIFEST.json missing or empty in $RUN_DIR" >&2
exit 1
fi
# Friendly node labels are written into the per-node subdirs (e.g.
# `orchestrator`, `stage-0`, ...). Count them, and check that
# orchestrator + every stage 0..N-1 are present. The label scheme is
# the collector's `bundle::assemble` contract.
node_dirs=()
while IFS= read -r -d '' dir; do
node_dirs+=("$(basename "$dir")")
done < <(find "$RUN_DIR" -mindepth 1 -maxdepth 1 -type d -print0 | sort -z)
echo "assert-bundle: node dirs: ${node_dirs[*]:-<none>}"
declare -A seen
for d in "${node_dirs[@]}"; do
seen["$d"]=1
done
missing=()
[ -n "${seen[orchestrator]:-}" ] || missing+=("orchestrator")
for ((i = 0; i < EXPECTED_STAGES; i++)); do
[ -n "${seen[stage-$i]:-}" ] || missing+=("stage-$i")
done
if [ "${#missing[@]}" -gt 0 ]; then
echo "assert-bundle: bundle missing nodes: ${missing[*]}" >&2
echo "(found: ${node_dirs[*]})" >&2
exit 1
fi
echo "assert-bundle: orchestrator + $EXPECTED_STAGES stages all present"
2026-05-29 08:55:10 +00:00
# (2) Each ACTOR node has at least one snapshot. The collector bundle
# layout (see `collector::bundle::assemble`) groups records into per-kind
2026-05-20 07:41:30 +00:00
# subdirs under each node label: `<label>/snapshots/snapshot-*.json`,
2026-05-29 08:55:10 +00:00
# `<label>/events/events-*.json`.
#
# The invariant is scoped to the orchestrator + stage actors that check
# (1) requires — not to every directory in the bundle. Telemetry-only
# nodes legitimately carry no snapshots: the vast.ai host sampler ships
# `vastai/` sample+log records under its own `vastai-stage-N` label, and
# those are host metrics, not actor-system state, so a snapshot is never
# produced for them. Looping over the known actor set keeps the check
# strict for the nodes that must have snapshots while tolerating these
# sidecar streams.
actor_nodes=(orchestrator)
for ((i = 0; i < EXPECTED_STAGES; i++)); do
actor_nodes+=("stage-$i")
done
for d in "${actor_nodes[@]}"; do
2026-05-20 07:41:30 +00:00
snap_count=$(find "$RUN_DIR/$d" -type f -name 'snapshot-*.json' | wc -l)
if [ "$snap_count" -lt 1 ]; then
echo "assert-bundle: $d has zero snapshots" >&2
echo "(node tree:" >&2
find "$RUN_DIR/$d" -maxdepth 3 >&2 || true
echo ")" >&2
exit 1
fi
done
2026-05-29 08:55:10 +00:00
echo "assert-bundle: every actor node (orchestrator + $EXPECTED_STAGES stages) has at least one snapshot"
2026-05-20 07:41:30 +00:00
# (3) Events span all three tiers. The collector persists batched events
# as `events-*.json`; each file is a JSON array of EventRecords whose
# `event.type` discriminator names the variant. The variant names are
# the API-shape contract from plan.md, stable across the diagnostics
# crate's lifetime.
# Match the pretty-printed shape `"type": "Variant"` produced by
# `serde_json::to_vec_pretty` (one space between key and value).
TIER1_PATTERN='"type": "\(SwimTransition\|DialStarted\|DialOutcome\|MessageSent\|MessageReceived\|SwimMetadataSent\|SwimMetadataReceived\)"'
TIER2_PATTERN='"type": "\(IrohConnTypeChanged\|RelayChanged\|ConnectionCacheHit\|ConnectionCacheMiss\|ConnectionCacheInvalidated\|NodeMapUpdate\)"'
TIER3_PATTERN='"type": "\(ProbeSent\|ProbeReceived\)"'
event_files=$(find "$RUN_DIR" -type f -name 'events-*.json' -print)
if [ -z "$event_files" ]; then
echo "assert-bundle: no events-*.json files in bundle" >&2
exit 1
fi
# shellcheck disable=SC2086
have_tier1=$(grep -l "$TIER1_PATTERN" $event_files 2>/dev/null | head -n 1 || true)
# shellcheck disable=SC2086
have_tier2=$(grep -l "$TIER2_PATTERN" $event_files 2>/dev/null | head -n 1 || true)
# shellcheck disable=SC2086
have_tier3=$(grep -l "$TIER3_PATTERN" $event_files 2>/dev/null | head -n 1 || true)
missing_tiers=()
[ -n "$have_tier1" ] || missing_tiers+=("Tier 1 (SWIM/dial/message)")
[ -n "$have_tier2" ] || missing_tiers+=("Tier 2 (iroh-internal)")
[ -n "$have_tier3" ] || missing_tiers+=("Tier 3 (probes)")
if [ "${#missing_tiers[@]}" -gt 0 ]; then
echo "assert-bundle: bundle missing event tiers: ${missing_tiers[*]}" >&2
echo "event files:" >&2
echo "$event_files" >&2
exit 1
fi
echo "assert-bundle: events span Tier 1, 2, 3"
# (4)+(5) Run the post-processor and inspect the summary.
POSTPROC_OUT="$WORKDIR/postproc-out"
mkdir -p "$POSTPROC_OUT"
if ! "$POSTPROC_BIN" "$BUNDLE" -o "$POSTPROC_OUT"; then
echo "assert-bundle: swactor-diag-postproc failed" >&2
exit 1
fi
SUMMARY="$POSTPROC_OUT/summary.md"
if [ ! -s "$SUMMARY" ]; then
echo "assert-bundle: summary.md is missing or empty" >&2
ls -lR "$POSTPROC_OUT" >&2
exit 1
fi
echo "assert-bundle: summary.md $(wc -c < "$SUMMARY") bytes, $(wc -l < "$SUMMARY") lines"
echo "assert-bundle: PASS"