#!/usr/bin/env bash # scripts/convert_and_test.sh — the whole thing, end to end. # # Regenerates the TypeScript server from the Python source through # every pipeline stage, then runs an externally-authored test suite # against the result and reports the score. # # This is the entry point the project lacked. Each stage already had # its own `tests/roundtrip.sh` oracle and the runtime already had its # Postman acceptance, but nothing ran them in order — so the claim # "the pipeline produces a server that passes the upstream suite" was # reproducible only by someone who knew which scripts to run and in # what sequence. Now it is one command. # # scripts/convert_and_test.sh [--quick] # # --quick skip the pipeline stages and only serve + test the # current goldens (what runtime/tests/postman.sh does # alone). Useful when iterating on the runtime. # # ONE-TIME SETUP (not repeated per run): # scripts/install_postgres.sh user-space PostgreSQL, no root # scripts/install_upstream.sh the upstream Python service # (cd tests && npm install) newman # (cd runtime && npm install) tsx + express # # Exit codes: # 0 every stage regenerated byte-identically AND the suite met the bar # 1 a stage diverged, or the suite fell below the bar # 2 setup missing set -u HERE="$(cd "$(dirname "$0")/.." && pwd)" cd "$HERE" QUICK=0 [ "${1:-}" = "--quick" ] && QUICK=1 BAR="${BAR:-280}" # tests/baseline.txt records 280/280 upstream fails=0 pass() { echo " PASS $1"; } fail() { echo " FAIL $1"; fails=$((fails + 1)); } # ---- 0. setup ------------------------------------------------------ echo "== 0. setup ==" missing="" [ -x tests/node_modules/.bin/newman ] || missing="$missing newman(tests/)" [ -d runtime/node_modules ] || missing="$missing runtime/node_modules" command -v python3 >/dev/null || missing="$missing python3" command -v node >/dev/null || missing="$missing node" [ -f tests/postman/Conduit.postman_collection.json ] \ || missing="$missing Conduit.postman_collection.json" if [ -n "$missing" ]; then echo " setup incomplete:$missing" >&2 echo " see the ONE-TIME SETUP block at the top of this script" >&2 exit 2 fi pass "toolchain and the upstream Postman collection are present" # ---- 1. the pipeline ---------------------------------------------- # Each stage reads the PREVIOUS stage's golden directory and rewrites # its own, then diffs against what was committed. A stage that passes # has reproduced its output byte-for-byte — which is what makes the # chain checkable rather than merely runnable. # ⚠ The decomposer reads the Python source through TEN compiled grammar # frontends (sgdl/F/bin/ccsf). Those are built artifacts, not # source — a fresh checkout does not have them, and this used to fail # with "ccsf1 not found" and no explanation of where it comes from. # # Everything downstream of the decomposer reads committed goldens, so # --quick exercises the emitter, assembler, bodies and the full # external suite without them. That is the difference between "you # cannot run this" and "you can run most of it today". FRONTENDS_MISSING="" for d in sgdl/F*/; do fam=$(basename "$d"); n="${fam%%_*}"; n="${n#F}" [ -x "$d/bin/ccsf$n" ] || FRONTENDS_MISSING="$FRONTENDS_MISSING ${fam%%_*}" done if [ "$QUICK" = 0 ] && [ -n "$FRONTENDS_MISSING" ]; then echo "== 1. pipeline: grammar frontends not built ==" echo " missing:$FRONTENDS_MISSING" echo echo " The decomposer stage needs one compiled frontend per grammar" echo " family. Each is produced by compiling its .sgr:" echo echo " sgdl//build/.sgr -> sgdl//bin/ccsf" echo echo " Build them all with one command:" echo echo " export B3U_BASE=... B3U_EMAIL=... B3U_PASSWORD=..." echo " scripts/build_frontends.sh" echo echo " Meanwhile --quick runs everything downstream, including the" echo " full external suite, from the committed goldens:" echo echo " scripts/convert_and_test.sh --quick" echo exit 2 fi if [ "$QUICK" = 0 ]; then echo "== 1. regenerate the TypeScript from the Python source ==" for stage in decomposer emitter assembler bodies; do if out=$(timeout 900 bash "$stage/tests/roundtrip.sh" 2>&1); then pass "$stage — output reproduced byte-identically" else fail "$stage — DIVERGED from its golden" echo "$out" | tail -12 | sed 's/^/ /' fi done else echo "== 1. pipeline SKIPPED (--quick) ==" fi # ---- 2. serve the result and run the external suite ---------------- # runtime/tests/postman.sh starts the emitted server (setsid, so the # tsx grandchildren die with it) and runs newman against the upstream # collection. The suite is not ours: it ships with the reference # Python implementation and encodes the RealWorld/Conduit API spec. echo "== 2. serve the emitted TypeScript and run the upstream suite ==" if out=$(BASELINE="$BAR" timeout 900 bash runtime/tests/postman.sh 2>&1); then score=$(echo "$out" | grep -oE '[0-9]+ / [0-9]+ assertions passing' | head -1) pass "external suite: ${score:-passed}" # ⚠ SURFACE THE DIVERGENCE WARNING. postman.sh's output is captured # into $out and, on success, only the score is echoed — so the # "assertion total differs" warning added on 2026-08-10 was printed # into a variable and dropped, invisible at the one entry point most # people actually run. A warning nobody sees is not a warning. echo "$out" | sed -n '/ASSERTION TOTAL DIFFERS/,/compare_to_upstream/p' \ | sed 's/^ */ /' else fail "external suite below the bar of $BAR" echo "$out" | tail -15 | sed 's/^/ /' fi # ---- 3. behaviour the external suite structurally cannot check ----- # The upstream collection runs "Feed" before "Follow Profile", so it # never sees a populated feed and cannot tell a correct feed from one # that returns everything (the original bug, which scored +13) or one # that returns nothing. And runtime/ is the hand-written adapter, so no # stage golden covers it either. Without this leg, both bugs are green. echo "== 3. behaviour the external suite cannot see ==" if out=$(timeout 300 bash runtime/tests/feed_follow.sh 2>&1); then pass "feed follows the follow graph" else fail "feed/follow behaviour is wrong" echo "$out" | grep -E "FAIL" | sed 's/^/ /' fi echo if [ "$fails" -eq 0 ]; then echo "P2T CONVERT + TEST: ALL PASS" # ⚠ --quick did NOT regenerate anything, so it must not say it did. # The original message claimed regeneration unconditionally and # printed that claim after a --quick run — the script overstating its # own result, which is exactly what the two-stage split exists to # prevent. if [ "$QUICK" = 0 ]; then echo " The TypeScript server was regenerated from the Python source" echo " and passed a test suite it did not author." else echo " The TypeScript that shipped in this archive passed a test" echo " suite it did not author. NOTHING WAS REGENERATED — run" echo " without --quick to reproduce it from the Python source." fi exit 0 else echo "P2T CONVERT + TEST: $fails FAILURE(S)" exit 1 fi