Root cause (UHLP U-GAP-025): the "greet check" in provision.sh was curl -s ... | grep -qE 'resulttype' against the raw body. ERROR payloads contain the literal key "resulttype" (e.g. {"resulttype":"error",...}), so grep always matched — every 7/7 battery pass was fake evidence.
Root cause (UHLP U-GAP-025): the "greet check" in provision.sh was curl -s ... | grep -qE 'result_type' against the raw body. ERROR payloads contain the literal key "result_type" (e.g. {"result_type":"error",...}), so grep always matched — every 7/7 battery pass was fake evidence.
Fix: a sourceable helper, assert_api.sh, with four parts:
curl -w '%{http_code}' writes the status to one temp file and the body (-o) to another. The body is never grepped for status.HTTP == 200 AND parsed result_type != "error" (checked after status, so a 000/missing body reports a status failure, not a JSON error).BASH_SOURCE guard — sourcing only defines functions; executing directly runs canned neg/pos self-tests with no live server.STATE_DIR arg — assert_persisted turns the old registry-only (in-memory) persistence check into a real on-disk check via prompts/*.prompt.json + workflows/*.workflow.json globs, validating each file's JSON too.#!/usr/bin/env bash
# assert_api.sh — sourceable HTTP+JSON e2e assertion helper (UHLP U-GAP-025)
# source ./assert_api.sh -> defines assert_api_ok / assert_persisted
# ./assert_api.sh [STATE_DIR] -> canned self-tests, no server needed
set -uo pipefail
SELF_TMP="" # global so the EXIT trap can reference it after main() returns
assert_json() { # <body_file> <http_code> <method> <url>
python3 - "$@" <<'PY'
import json, sys
body_file, code, method, url = sys.argv[1], sys.argv[2], sys.argv[3], sys.argv[4]
if code != "200": # status FIRST
print(f"FAIL [{method} {url}] HTTP {code} (expected 200)", file=sys.stderr)
sys.exit(1)
try:
with open(body_file, "rb") as f:
data = json.load(f)
except Exception as e:
print(f"FAIL [{method} {url}] body is not valid JSON: {e}", file=sys.stderr)
sys.exit(1)
rt = data.get("result_type", "") if isinstance(data, dict) else ""
if str(rt).lower() == "error": # parsed value, not regex
print(f"FAIL [{method} {url}] result_type == 'error': {json.dumps(data)[:200]}", file=sys.stderr)
sys.exit(1)
print(f"PASS [{method} {url}] HTTP {code} result_type={rt!r}")
PY
}
assert_api_ok() { # <method> <url> [json-body]
local method="$1" url="$2" body="${3:-}"
local dir out code
dir="$(mktemp -d)"; out="$dir/body"; code="$dir/code"
local curl_args=(-sS -o "$out" -w '%{http_code}' -X "$method")
[[ -n "$body" ]] && curl_args+=(-H 'Content-Type: application/json' -d "$body")
curl "${curl_args[@]}" "$url" > "$code" || true # status -> temp file
local rc=0
if [[ ! -s "$code" ]]; then
echo "FAIL [$method $url] curl produced no HTTP status (connect error?)" >&2; rc=1
elif ! [[ "$(cat "$code")" =~ ^[0-9]{3}$ ]]; then
echo "FAIL [$method $url] curl produced non-status '$(cat "$code")'" >&2; rc=1
else
assert_json "$out" "$(cat "$code")" "$method" "$url" || rc=1
fi
rm -rf "$dir"; return "$rc"
}
assert_persisted() { # <STATE_DIR> — real on-disk persistence check
local state_dir="${1:?assert_persisted: STATE_DIR argument required}"
local fail=0 n_p n_w f
n_p="$(compgen -G "$state_dir/prompts/*.prompt.json" 2>/dev/null | wc -l || true)"
n_w="$(compgen -G "$state_dir/workflows/*.workflow.json" 2>/dev/null | wc -l || true)"
[[ "$n_p" -gt 0 ]] || { echo "FAIL persisted: no prompts/*.prompt.json under $state_dir" >&2; fail=1; }
[[ "$n_w" -gt 0 ]] || { echo "FAIL persisted: no workflows/*.workflow.json under $state_dir" >&2; fail=1; }
for f in "$state_dir"/prompts/*.prompt.json "$state_dir"/workflows/*.workflow.json; do
[[ -e "$f" ]] || continue
assert_json "$f" 200 "file" "$f" || fail=1
done
(( fail )) && { echo "FAIL persisted $state_dir" >&2; return 1; }
echo "PASS persisted $state_dir ($n_p prompts / $n_w workflows on disk)"
}
main() { # canned neg/pos self-tests, no live server
local state_dir="${1:-}" pass=0 fail=0
SELF_TMP="$(mktemp -d)"; trap 'rm -rf "$SELF_TMP"' EXIT
t() { if "$@"; then pass=$((pass+1)); else fail=$((fail+1)); fi; }
tn() { if "$@"; then fail=$((fail+1)); else pass=$((pass+1)); fi; }
echo "== assert_api.sh canned self-tests (no server) =="
printf '{"result_type":"ok","message":"hi"}' > "$SELF_TMP/pos.json"
t assert_json "$SELF_TMP/pos.json" 200 GET http://<ip-address>:9/greet
# U-GAP-025 regression: error payload still contains literal "result_type"
printf '{"result_type":"error","error":"boom","hint":"the key result_type appears in error payloads too"}' > "$SELF_TMP/err.json"
tn assert_json "$SELF_TMP/err.json" 200 GET http://<ip-address>:9/greet
printf '{"result_type":"ok"}' > "$SELF_TMP/five.json"; tn assert_json "$SELF_TMP/five.json" 500 GET http://<ip-address>:9/x
printf 'definitely-not-json' > "$SELF_TMP/bad.json"; tn assert_json "$SELF_TMP/bad.json" 200 GET http://<ip-address>:9/x
: > "$SELF_TMP/empty.json"; tn assert_json "$SELF_TMP/empty.json" 200 GET http://<ip-address>:9/x
tn assert_api_ok GET http://<ip-address>:9/greet # port 9 refused
if [[ -n "$state_dir" ]]; then
mkdir -p "$SELF_TMP/ok/prompts" "$SELF_TMP/ok/workflows"
printf '{"result_type":"ok"}' > "$SELF_TMP/ok/prompts/a.prompt.json"
printf '{"result_type":"ok"}' > "$SELF_TMP/ok/workflows/b.workflow.json"
t assert_persisted "$SELF_TMP/ok"
mkdir -p "$SELF_TMP/bad/prompts"
printf '{"result_type":"ok"}' > "$SELF_TMP/bad/prompts/a.prompt.json"
tn assert_persisted "$SELF_TMP/bad" # workflows glob missing
mkdir -p "$SELF_TMP/err/prompts" "$SELF_TMP/err/workflows"
printf '{"result_type":"error"}' > "$SELF_TMP/err/prompts/a.prompt.json"
printf '{"result_type":"ok"}' > "$SELF_TMP/err/workflows/b.workflow.json"
tn assert_persisted "$SELF_TMP/err" # persisted file is an error
fi
echo; echo "assert_api.sh self-test: $pass passed, $fail failed"
(( fail == 0 ))
}
if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then main "$@"; fi # BASH_SOURCE guard
provision.sh then becomes an honest 8-test battery on top of the helper (defaults BASE_URL=http://<ip-address>:8080, STATE_DIR=<script dir>/.state):
#!/usr/bin/env bash
set -euo pipefail
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$HERE/assert_api.sh"
BASE="${BASE_URL:-http://<ip-address>:8080}"
STATE_DIR="${STATE_DIR:-$HERE/.state}"
pass=0; fail=0
run() { if "$@"; then pass=$((pass+1)); else fail=$((fail+1)); fi; }
echo "== provision battery vs $BASE (STATE_DIR=$STATE_DIR) =="
run assert_api_ok GET "$BASE/greet"
run assert_api_ok GET "$BASE/greet?name=provision"
run assert_api_ok POST "$BASE/registry" '{"kind":"prompt","name":"sys.greet","content":"hello"}'
run assert_api_ok POST "$BASE/registry" '{"kind":"workflow","name":"wf.greet","steps":["sys.greet"]}'
run assert_api_ok GET "$BASE/registry"
run assert_api_ok POST "$BASE/registry" '{"kind":"prompt","name":"sys.greet","content":"hello"}' # idempotent upsert
run assert_api_ok GET "$BASE/registry/prompt/sys.greet"
run assert_persisted "$STATE_DIR" # real on-disk check
echo; echo "battery: $pass passed / $fail failed (8 total)"
(( fail == 0 ))
All runs in /tmp/e2e-fix (bash 5 / python 3.14 / curl 8.18; bash -n clean on both scripts).
1. Canned neg/pos self-tests — no live server (mode 1: 6/6, mode 2 with STATE_DIR: 9/9, exit 0):
PASS [GET http://<ip-address>:9/greet] HTTP 200 result_type='ok'
FAIL [GET http://<ip-address>:9/greet] result_type == 'error': {"result_type": "error", ...}
FAIL [GET http://<ip-address>:9/x] HTTP 500 (expected 200)
FAIL [GET http://<ip-address>:9/x] body is not valid JSON: Expecting value: line 1 column 1 (char 0)
FAIL [GET http://<ip-address>:9/x] HTTP 000 (expected 200) # port 9 refused
...
assert_api.sh self-test: 6 passed, 0 failed # 9 passed, 0 failed with STATE_DIR arg
The U-GAP-025 regression payload — an ERROR body whose text contains the literal substring result_type — is caught because the assertion is on the parsed value, not the raw string.
2. Live battery vs fixture registry (real HTTP): 8/8, exit 0, with per-check evidence:
PASS [GET http://<ip-address>:45693/greet] HTTP 200 result_type='ok' (x2 greet)
PASS [POST http://<ip-address>:45693/registry] HTTP 200 result_type='ok' (prompt, workflow)
PASS [GET http://<ip-address>:45693/registry] HTTP 200 result_type='ok'
PASS [POST ...] HTTP 200 result_type='ok' (idempotent upsert)
PASS [GET .../registry/prompt/sys.greet] HTTP 200 result_type=''
PASS persisted /tmp/e2e-fix/.state (1 prompts / 1 workflows on disk)
battery: 8 passed / 0 failed (8 total)
On-disk evidence (what #8 actually checked, not an in-memory registry):
/tmp/e2e-fix/.state/prompts/sys.greet.prompt.json -> {"kind": "prompt", "name": "sys.greet", "content": "hello"}
/tmp/e2e-fix/.state/workflows/wf.greet.workflow.json
3. Decisive U-GAP-025 repro — live server returns HTTP 200 with result_type:error:
raw body: {"result_type": "error", "error": "boom"}
old grep -qE 'result_type' -> PASS (FAKE EVIDENCE)
new helper -> FAIL [GET .../greet] result_type == 'error': {...} exit=1
4. Negative-path honesty: pointed the battery at a real foreign service (already bound to ports 8080/18080 in this sandbox, answering 401): battery reported 0 passed / 8 failed with each line citing the actual HTTP 401 and empty on-disk globs — exit 1. A wrong or absent server can no longer be mistaken for a pass.
Edge cases tested: HTTP 200 + ok, HTTP 200 + result_type:error (the gap), HTTP 500, HTTP 401 (live foreign), HTTP 000/connection refused, non-JSON body, empty body, missing body file, missing workflows/ glob, persisted file with result_type:error, sourced vs executed guard (battery output contains no self-test banner), and set -e interplay (all failing commands are guarded with || true/if so provision.sh reports counts instead of dying mid-run).
{"problem_class":"shell-e2e-verification-script","model":"deepseek-v4-flash","result":"passed","tests":17}
(tests = 8 live battery + 9 canned self-tests; the live battery claim is 8/8 with real curl-status/parsed-JSON/on-disk evidence.)
Solved by Pi Agent (deepseek-v4-flash).