#!/usr/bin/env bash # ka-promote test suite. # # Each test runs ka-promote against a fixture from fixtures/ in a temporary # sandboxed kernel-agent tree (bin/, fleet/, patches/ — patches/ symlinked # from the real repo so we exercise the real scope-tagged patch files). # # Exit 0 iff every test passes. Non-zero on first failure. set -euo pipefail repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" fixtures="${repo_root}/tests/ka-promote/fixtures" # Phase-3 ground truth — re-recorded 2026-05-19 after issue #31 fix # (write_cumulative now strips per-input trailers + emits canonical # separators between, but not after, concatenated patches). FRESNEL_EXPECTED_B2SUM=9c21751cc48ab57cdf48058cc4309752de169c567bbb898c342ff3e4a5cc79add53e3fd4217c2ae2ae7c16b0f19518cf1791907367e1ea9ef16458e1e90c05e0 pass=0 fail=0 results=() note() { printf ' %s\n' "$*"; } ok() { results+=("PASS $1"); pass=$((pass+1)); note "PASS"; } ko() { results+=("FAIL $1: $2"); fail=$((fail+1)); note "FAIL: $2"; } make_sandbox() { # $1 = fixture yaml path. Builds a scratch tree with bin/+fleet/+patches/ # and copies the fixture into fleet/.yaml (extracting host from fixture). local fixture="$1" local scratch scratch=$(mktemp -d -t ka-promote-test.XXXXXX) mkdir -p "$scratch/bin" "$scratch/fleet" cp "$repo_root/bin/ka-promote" "$scratch/bin/ka-promote" ln -s "$repo_root/patches" "$scratch/patches" local host host=$(python3 -c "import yaml,sys; print(yaml.safe_load(open(sys.argv[1]))['host'])" "$fixture") cp "$fixture" "$scratch/fleet/${host}.yaml" echo "$scratch|$host" } run_test() { local name="$1" local fixture="$2" local expected_exit="$3" local check_fn="${4:-}" echo "::: $name" local pair scratch host pair=$(make_sandbox "$fixture") scratch="${pair%|*}" host="${pair#*|}" set +e out=$("$scratch/bin/ka-promote" "$host" --output-dir "$scratch/build" 2>&1) actual_exit=$? set -e if [ "$actual_exit" -ne "$expected_exit" ]; then ko "$name" "expected exit $expected_exit, got $actual_exit. Output: $out" rm -rf "$scratch" return fi if [ -n "$check_fn" ]; then if ! "$check_fn" "$scratch" "$host" "$out"; then ko "$name" "check function reported failure (see notes above)" rm -rf "$scratch" return fi fi ok "$name" rm -rf "$scratch" } check_series_dir() { # 7 patches resolved, all from_series:true, in filename order. local scratch="$1" host="$2" local lock="$scratch/build/$host/v7.0/manifest.lock" [ -f "$lock" ] || { note "manifest.lock missing"; return 1; } local n n=$(python3 -c "import yaml; print(len(yaml.safe_load(open('$lock'))['resolved_patches']))") if [ "$n" -ne 7 ]; then note "expected 7 resolved patches, got $n" return 1 fi python3 - "$lock" <<'PY' || { note "from_series check failed"; exit 1; } import yaml, sys lk = yaml.safe_load(open(sys.argv[1])) for r in lk['resolved_patches']: assert r['from_series'] is True, f"{r['include']} not flagged from_series" expected_basenames = [f'0{i:03d}'[1:] for i in range(1,8)] # 0001..0007 got = [r['include'].split('/')[-1][:4] for r in lk['resolved_patches']] assert got == ['000'+str(i) for i in range(1,8)], f'apply order mismatch: {got}' PY } check_fresnel_parity() { local scratch="$1" host="$2" local lock="$scratch/build/$host/v7.0/manifest.lock" [ -f "$lock" ] || { note "manifest.lock missing"; return 1; } local b2 b2=$(python3 -c "import yaml; print(yaml.safe_load(open('$lock'))['cumulative']['b2sum'])") if [ "$b2" != "$FRESNEL_EXPECTED_B2SUM" ]; then note "b2sum mismatch" note " expected: $FRESNEL_EXPECTED_B2SUM" note " got: $b2" return 1 fi } echo echo "Running ka-promote test suite from $repo_root" echo # ----- unit: strip_trailer + write_cumulative shape (issue #31) ----- echo "::: strip_trailer + cumulative shape (issue #31)" python3 - "$repo_root" <<'PY' import importlib.util, pathlib, sys, tempfile, os root = pathlib.Path(sys.argv[1]) from importlib.machinery import SourceFileLoader mod = SourceFileLoader("ka_promote", str(root/"bin"/"ka-promote")).load_module() # strip_trailer accepts both shapes and yields newline-terminated body assert mod.strip_trailer(b"...body...\n-- \n2.54.0\n\n") == b"...body...\n" assert mod.strip_trailer(b"...body...\n-- \n2.53.0\n\n") == b"...body...\n" assert mod.strip_trailer(b"...body...\n-- \n2.20\n\n") == b"...body...\n" assert mod.strip_trailer(b"...body...\n") == b"...body...\n" assert mod.strip_trailer(b"...body...") == b"...body...\n" # Multiple trailing blanks after the version still strip assert mod.strip_trailer(b"x\n-- \n2.54.0\n\n\n") == b"x\n" # write_cumulative: 3 inputs (mix of with-/without-trailer), check ordering with tempfile.TemporaryDirectory() as d: p1 = os.path.join(d, "a.patch"); open(p1,"wb").write(b"PA\n-- \n2.54.0\n\n") p2 = os.path.join(d, "b.patch"); open(p2,"wb").write(b"PB\n") # already bare p3 = os.path.join(d, "c.patch"); open(p3,"wb").write(b"PC\n-- \n2.40.1\n\n") out = os.path.join(d, "out.patch") resolved = [{"src": p1}, {"src": p2}, {"src": p3}] mod.write_cumulative(resolved, out) body = open(out,"rb").read() assert body == b"PA\n-- \n2.54.0\n\nPB\n-- \n2.54.0\n\nPC\n", repr(body) # Last patch (PC) must NOT carry an orphan trailer at EOF assert not body.rstrip(b"\n").endswith(b"2.40.1"), \ f"last patch's trailer leaked into cumulative: {body[-40:]!r}" print("PASS") PY if [ $? -eq 0 ]; then results+=("PASS strip_trailer + cumulative shape (issue #31)") pass=$((pass+1)) else results+=("FAIL strip_trailer + cumulative shape (issue #31)") fail=$((fail+1)) fi echo # Use the real fleet/fresnel.yaml — copy into a sandbox so the test is hermetic. mkdir -p /tmp/ka-promote-parity-fixture cp "$repo_root/fleet/fresnel.yaml" /tmp/ka-promote-parity-fixture/fresnel.yaml run_test "fresnel parity (= Phase-3 ground truth b2sum)" \ /tmp/ka-promote-parity-fixture/fresnel.yaml 0 check_fresnel_parity rm -rf /tmp/ka-promote-parity-fixture run_test "series-dir resolver (bes2600/staging-prep-series-danctnix → 7 patches)" \ "$fixtures/series-dir.yaml" 0 check_series_dir run_test "bad-include rejection (exit 4)" \ "$fixtures/bad-include.yaml" 4 run_test "missing-patch rejection (exit 2)" \ "$fixtures/missing-patch.yaml" 2 run_test "duplicate-include rejection (exit 4)" \ "$fixtures/duplicate-include.yaml" 4 echo echo "====================" printf '%s\n' "${results[@]}" echo "====================" echo "passed: $pass" echo "failed: $fail" [ "$fail" -eq 0 ] || exit 1