Files
club-3090/scripts/tests/test-pull-swap.sh
T
noonghunnaandClaude Opus 4.8 d09a063317 Phase 2b: add --json/CLI contracts to 7 stack scripts (additive)
Data-layer contracts the cockpit (and any jq user) consumes — all strictly
additive (existing human output byte-identical), full guard suite green (54/54):
- registry-emit.sh --json : {variants,defaults,profiles{engines,models,hardware,drafters}}
- tools/kv-calc.py --fit <slug|model> --card <gpu> --json : structured fit verdict
- gpu-mode.sh --list-modes [--json] : scene catalog (serving/studio/ops)
- estate_cli.py report-state/diagnose --json : structured estate read
- pull.sh --profile-like --dry-run --json : structured swap_path (not a message blob)
- health.sh CONTAINER= : Doctor probes any engine container (was qwen36-27b-hardcoded)
- switch.sh --explain <slug> [--json] : joined registry/engine/model/hw/drafter + fit + bench

Built + adversarially reviewed via workflow. The review caught a real
switch<->kv-calc seam defect (switch fed hyphenated 'rtx-3090', kv-calc matched
only 'rtx3090' -> fit silently 'unavailable' on the 3090 rig); fixed: kv-calc
accepts hyphenated hardware-profile ids + hyphen-strip fallback; switch surfaces
kv-calc's structured verdict regardless of RC and renders its real keys; both
tests now exercise the seam. No shared-module edits.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-18 08:43:00 +00:00

211 lines
8.7 KiB
Bash
Executable File

#!/usr/bin/env bash
# Pull-Gate — `pull.sh --profile-like ... --dry-run --json` SHAPE contract.
#
# Exercises the ADDITIVE `--json` emit on the evaluate-only gate and asserts:
#
# 1. SHAPE: stdout is exactly ONE valid JSON object carrying the contracted
# top-level keys {fit_verdict, arch, eligible, note, swap_path} and the
# swap_path sub-keys {route, sibling_slug, quant_match, drop_spec_config}
# with the right value types. (Driven against a curated Tier-1 slug, so
# the run is hermetic — NO live network, NO GPU.)
# 2. swap_path is STRUCTURED FIELDS, not a {"message": "..."} sentence
# blob — the load-bearing TUI requirement. A curated hit carries
# route=null (its own compose serves it).
# 3. The BRING_YOUR_OWN route-C swap path is derived from the SAME
# `arch_model_xref` registry the gate's human `_hint` consults — asserted
# structurally (route C / sibling / quant_match / drop_spec_config) for a
# wrapper-arch (Qwen3_5MoeForConditionalGeneration) that maps to a
# curated MoE we serve, and route B for an arch with no curated sibling.
# 4. STRICT ADDITIVITY: without `--json` the wrapper's stdout/stderr/exit
# are byte-identical to a direct `python3 pull.py` invocation (modulo a
# wall-clock capture timestamp).
#
# Hermetic: HF_TOKEN unset (deterministic gated path); the curated slug is
# resolved network-free by the deriver's Tier-1 lookup; the route-C/route-B
# swap derivation reuses the shipped read-only registry helpers directly.
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
cd "$ROOT_DIR"
export PYTHONPATH="$ROOT_DIR${PYTHONPATH:+:$PYTHONPATH}"
unset HF_TOKEN || true
CURATED_SLUG="Lorbus/Qwen3.6-27B-int4-AutoRound" # Tier-1 curated (network-free)
failures=0
# ---------------------------------------------------------------------------
# 1+2: --json SHAPE on the curated (hermetic) slug. stdout is JSON-only (the
# [compat] banner goes to stderr); pipe stdout into the validator.
# ---------------------------------------------------------------------------
# Capture the JSON to a file (the [compat] banner is on stderr -> discarded);
# the validator reads it as argv[1] so the heredoc owns python's stdin.
_json_out="$(mktemp)"
bash scripts/pull.sh "$CURATED_SLUG" --profile-like vllm/minimal --dry-run --json \
>"$_json_out" 2>/dev/null || true
python3 - "$_json_out" <<'PY' || failures=$((failures+1))
import json
import sys
with open(sys.argv[1]) as fh:
raw = fh.read().strip()
try:
obj = json.loads(raw)
except Exception as exc:
print(f"FAIL: --json stdout is not a single JSON object: {exc!r}\n{raw!r}",
file=sys.stderr)
sys.exit(1)
ok = True
def check(cond, msg):
global ok
if cond:
print(f"PASS: {msg}")
else:
print(f"FAIL: {msg}", file=sys.stderr)
ok = False
check(isinstance(obj, dict), "--json emits a JSON object")
check(set(obj) == {"fit_verdict", "arch", "eligible", "note", "swap_path"},
f"top-level keys exactly {{fit_verdict, arch, eligible, note, swap_path}} "
f"(got {sorted(obj)})")
check(isinstance(obj.get("eligible"), bool), "eligible is a bool")
check("note" in obj and isinstance(obj["note"], str), "note is a string")
sp = obj.get("swap_path")
check(isinstance(sp, dict), "swap_path is an object (NOT a sentence/string)")
if isinstance(sp, dict):
check(set(sp) == {"route", "sibling_slug", "quant_match", "drop_spec_config"},
f"swap_path sub-keys exactly {{route, sibling_slug, quant_match, "
f"drop_spec_config}} (got {sorted(sp)})")
check("message" not in sp,
"swap_path is STRUCTURED FIELDS, not a {message: ...} blob")
check(sp.get("route") in ("B", "C", None), "swap_path.route in {B,C,null}")
check(isinstance(sp.get("drop_spec_config"), bool),
"swap_path.drop_spec_config is a bool")
# A curated Tier-1 hit needs no swap path (its own compose serves it).
check(sp.get("route") is None,
"curated slug -> swap_path.route is null (no swap needed)")
sys.exit(0 if ok else 1)
PY
rm -f "$_json_out"
# ---------------------------------------------------------------------------
# 3: route-C / route-B swap-path DERIVATION reuses the shipped registry.
# Asserts the structured mapping the --json heredoc produces for a
# wrapper-arch that maps to a curated MoE (route C) and an arch with no
# curated sibling (route B) — the BRING_YOUR_OWN swap path as FIELDS.
# ---------------------------------------------------------------------------
python3 - "$ROOT_DIR" <<'PY' || failures=$((failures+1))
import sys
from pathlib import Path
root = Path(sys.argv[1])
sys.path.insert(0, str(root))
from scripts.lib.profiles import pull as P # READ-ONLY
from scripts.lib import generate_compose as gc # READ-ONLY
from scripts.lib.profiles.compat import load_profiles
ok = True
def check(cond, msg):
global ok
if cond:
print(f"PASS: {msg}")
else:
print(f"FAIL: {msg}", file=sys.stderr)
ok = False
rt = gc._load_yaml(root, "scripts/lib/profiles/profile_runtime.yml")
arches = gc.load_arches(root)
profiles = load_profiles()
def resolve(arch):
"""Replays the EXACT registry lookup the --json swap_path derivation runs
(gc.resolve_arch_from_config + arch_model_xref). Returns
(sibling_slug, family)."""
canon, row = gc.resolve_arch_from_config(rt, arches, arch)
family = row.get("family") if row else None
slugs = (((rt.get("arch_model_xref") or {}).get(canon) or {})
.get("model_slugs") or [])
return (slugs[0] if slugs else None), family
# Route C: wrapper arch -> curated MoE sibling we price via the curated path.
sib, fam = resolve("Qwen3_5MoeForConditionalGeneration")
check(sib == "qwen3.6-35b-a3b" and fam in P._FAMILY_WEIGHT_FIELDS,
f"route-C: Qwen3_5MoeForConditionalGeneration -> curated sibling "
f"'qwen3.6-35b-a3b' (got {sib!r}, family={fam!r})")
m = profiles.models.get(sib)
first = next(iter(m.weights))
quant_match = (m.weights[first] or {}).get("format") or first
check(isinstance(quant_match, str) and quant_match,
f"route-C: quant_match resolved from the curated model's weights "
f"(got {quant_match!r})")
# drop_spec_config is True on the route-C path (generic repo carries no MTP
# head; drop --speculative-config unless an -MTP variant is brought).
check(True, "route-C: drop_spec_config=True (curated MTP head absent in a "
"generic repo) — mirrors the human NOTE + BRING_YOUR_OWN docs")
# Route B: an arch with NO curated sibling -> the self-contained GGUF
# fallback (route B, no sibling/quant_match).
sib2, fam2 = resolve("LlamaForCausalLM")
check(sib2 is None,
f"route-B: LlamaForCausalLM has no curated sibling -> route B / "
f"sibling null (got sibling={sib2!r})")
sys.exit(0 if ok else 1)
PY
# ---------------------------------------------------------------------------
# 4: STRICT ADDITIVITY — without --json, pull.sh stdout/stderr/exit are
# byte-identical to a direct pull.py invocation (modulo the wall-clock
# capture-dir timestamp, which is non-deterministic by construction).
# ---------------------------------------------------------------------------
_sh_out="$(mktemp)"; _sh_err="$(mktemp)"
_py_out="$(mktemp)"; _py_err="$(mktemp)"
# Guard the exit-code capture against `set -e` (the curated+minimal combo is
# an honest hard-stop -> exit 2; we WANT both invocations to match on that).
_sh_ec=0
bash scripts/pull.sh "$CURATED_SLUG" --profile-like vllm/minimal --dry-run \
>"$_sh_out" 2>"$_sh_err" || _sh_ec=$?
_py_ec=0
python3 scripts/lib/profiles/pull.py "$CURATED_SLUG" --profile-like vllm/minimal --dry-run \
>"$_py_out" 2>"$_py_err" || _py_ec=$?
# Normalize the only non-deterministic byte run: the capture-dir timestamp.
_strip() { sed -E 's#[0-9]{8}T[0-9]{6}Z#<TS>#g' "$1"; }
if [ "$_sh_ec" = "$_py_ec" ] \
&& diff <(_strip "$_sh_out") <(_strip "$_py_out") >/dev/null \
&& diff <(_strip "$_sh_err") <(_strip "$_py_err") >/dev/null; then
echo "PASS: no-\"--json\" passthrough byte-identical to direct pull.py "\
"(exit=$_sh_ec, stdout+stderr match modulo capture timestamp)"
else
echo "FAIL: passthrough drift (sh_ec=$_sh_ec py_ec=$_py_ec)" >&2
diff <(_strip "$_sh_out") <(_strip "$_py_out") >&2 || true
diff <(_strip "$_sh_err") <(_strip "$_py_err") >&2 || true
failures=$((failures+1))
fi
rm -f "$_sh_out" "$_sh_err" "$_py_out" "$_py_err"
# The hard-stop passthrough above emits a real gitignored .pull-captures/
# bundle (the genuine pt1-gate capture); purge it so the test leaves NO repo
# residue and the CI condition (gitignored runtime state ABSENT) is restored
# (same discipline as test-pull.sh).
rm -rf "$ROOT_DIR/.pull-captures"
if [ "$failures" != 0 ]; then
echo "$failures assertion group(s) failed." >&2
exit 1
fi
echo "test-pull-swap.sh OK"