#!/bin/bash # ============================================================================ # The assertions scripts/e2e.sh makes after the scenarios have replayed. # # Split out of e2e.sh at the 250-line cap. A filing change, not an interface # change: these are sourced, so they still run against the same fake server # and the same bot process the scenarios used. # ============================================================================ # The gate that did not exist on 2026-08-29, proven against the real process: # with signal-cli unable to reach Signal, /healthz must go red rather than # keep answering 200 while nothing arrives. prove_the_deafness_gate() { local health="http://127.0.0.1:${BOT_PORT}/healthz" echo "== cutting the fake's connection to Signal" curl -sf -X POST "${FAKE_URL}/_control/connection" \ -H 'Content-Type: application/json' -d '{"alive": false}' >/dev/null local code="" waited=0 while ((waited < 150)); do code="$(curl -s -o /dev/null -w '%{http_code}' "$health")" [[ "$code" == "503" ]] && break sleep 1 waited=$((waited + 1)) done [[ "$code" == "503" ]] || die "/healthz still answers ${code} with the connection cut" curl -s "$health" | grep -q "Closed unexpectedly" || die "/healthz went red without saying why" echo "== /healthz turned 503 and named the reason after ${waited}s" echo "== restoring the connection" curl -sf -X POST "${FAKE_URL}/_control/connection" \ -H 'Content-Type: application/json' -d '{"alive": true}' >/dev/null waited=0 while ((waited < 150)); do code="$(curl -s -o /dev/null -w '%{http_code}' "$health")" [[ "$code" == "200" ]] && break sleep 1 waited=$((waited + 1)) done [[ "$code" == "200" ]] || die "/healthz stayed ${code} after the connection came back" echo "== /healthz recovered on its own after ${waited}s" } # A person tagged while the bot is up becomes a command after POST /_reload, # with no restart: the same process that just answered "Nie znam !delta" # posts delta's photo a moment later. The scenarios live next to the main # set but outside its glob, because the second one is only true after the # reload that this proof performs between them. prove_the_table_reloads_without_a_restart() { local proofs="${REPO_ROOT}/fake_signal/proofs" echo "== before the reload, !delta is nobody" replay "${proofs}/reload_before.yaml" echo "== tagging a fifth person into the pack, then scripts/reload.sh" "$PYTHON" "$REPO_ROOT/tool/e2e_pack.py" \ --dir "${TEMP_DIR}/pack" --group "$WATCHED_GROUP" --extra delta >/dev/null # The operator's script, not a bare curl: this is the one place it runs # against a real process in CI, so its exit code is the proof. BIND_PORT="$BOT_PORT" "$REPO_ROOT/scripts/reload.sh" || die "scripts/reload.sh did not get the new table live" echo "== after the reload, !delta posts a photo" replay "${proofs}/reload_after.yaml" } # A switch thrown from the management page is read on the very next attempt: # no reload, no restart. The PUT here is what the page's Save button sends, # aimed at the bot directly; the two scenarios bracket it the same way the # reload pair does. Not a timed switch, because a proof that depends on the # clock has to wait on it, and the unit tests already cover the deadline. prove_a_switch_is_live_without_a_reload() { local proofs="${REPO_ROOT}/fake_signal/proofs" local switch="http://127.0.0.1:${BOT_PORT}/_commands/cat" echo "== switching !kot off" curl -sf -X PUT "$switch" -H 'Content-Type: application/json' \ -d '{"enabled": false}' | grep -q '"enabled":false' || die "PUT /_commands/cat did not report the command off" replay "${proofs}/switch_off.yaml" echo "== switching !kot back on" curl -sf -X PUT "$switch" -H 'Content-Type: application/json' \ -d '{"enabled": true}' | grep -q '"enabled":true' || die "PUT /_commands/cat did not report the command on" replay "${proofs}/switch_on.yaml" } # The operator's alias file is data the table is built from, so it takes # the same path as a tagged person: written, then scripts/reload.sh. The # file is the one CUSTOM_ALIASES_FILE points the bot at, never the # operator's own; a name the table does not hold is what the second half # proves the bot refuses, with the old table still answering. prove_an_alias_is_live_after_a_reload() { local proofs="${REPO_ROOT}/fake_signal/proofs" local file="${TEMP_DIR}/aliases_custom.json" echo "== before the alias, !e2ekot is nobody" replay "${proofs}/alias_before.yaml" echo "== writing the operator's alias file, then scripts/reload.sh" printf '{"cat": ["!e2ekot"]}\n' > "$file" BIND_PORT="$BOT_PORT" "$REPO_ROOT/scripts/reload.sh" || die "scripts/reload.sh did not get the alias live" replay "${proofs}/alias_after.yaml" echo "== an alias for a command that does not exist keeps the old table" printf '{"cat": ["!e2ekot"], "nosuch": ["!nic"]}\n' > "$file" local answer answer="$(curl -s -w '%{http_code}' -X POST "http://127.0.0.1:${BOT_PORT}/_reload")" [[ "$answer" == *nosuch*409 ]] || die "a bad alias file was not kept out with 409 and its name: ${answer}" replay "${proofs}/alias_kept.yaml" printf '{"cat": ["!e2ekot"]}\n' > "$file" } # A budget, not a benchmark. This run was four minutes until the liveness # tick became configurable, and nothing announced the cost -- slower is the # one regression a green suite never reports. Exceeding it fails the run; # whoever exceeded it decides whether the check earned its seconds or the # budget moves. E2E_BUDGET_SECONDS raises it for a loaded machine. # What the !dodaj replies cannot prove on their own: every queued file was # stripped before it waited anywhere, a typed label reached nothing but the # queue, the refused and out-of-scope sends wrote nothing -- and then the one # door into the album, taken through the management page, after which the # bot answers with the photo and recognises a second copy of it. prove_the_add_photo_queue() { local pack="${TEMP_DIR}/pack" echo "== checking every queued file was stripped before it waited" "$PYTHON" "$REPO_ROOT/tool/e2e_media.py" --verify-pack "$pack" echo "== checking a typed label reached nothing but the queue" if [[ -e "${pack}/custom.json" ]]; then echo "Error: custom.json was written before anything was approved" >&2 exit 1 fi if grep -rq "piesek" "$pack"; then echo "Error: a label survived a message that carried no attachment" >&2 exit 1 fi echo "== checking exactly the accepted sends are waiting" local queued queued="$(find "${pack}/pending" -type f ! -name '*.json' | wc -l)" if [[ "$queued" != "5" ]]; then echo "Error: expected 5 queued files (person, custom, heic, svg, video), found ${queued}" >&2 exit 1 fi echo "== checking signal-cli's own copies were deleted, and only those" local left left="$(curl -sf "${FAKE_URL}/_control/attachments")" for taken in att-person att-custom att-heic att-svg att-video att-junk; do if [[ "$left" == *"$taken"* ]]; then echo "Error: ${taken} is still in the container store: ${left}" >&2 exit 1 fi done # Out of scope means never fetched: the second group's file is untouched. if [[ "$left" != *"att-second"* ]]; then echo "Error: a send in a group without the album was fetched: ${left}" >&2 exit 1 fi echo "== approving the queue through the management page" "$PYTHON" "$REPO_ROOT/tool/e2e_approve.py" --manage-url "$MANAGE_URL" echo "== checking the album took them whole, and the queue is empty" "$PYTHON" "$REPO_ROOT/tool/e2e_media.py" --verify-pack "$pack" if [[ -n "$(ls -A "${pack}/pending")" ]]; then echo "Error: the queue is not empty after approving everything" >&2 exit 1 fi if grep -qE '"(wakacje|piesek)"[[:space:]]*:' "${pack}/custom.json"; then echo "Error: a typed label became a key in custom.json" >&2 exit 1 fi if ! grep -q '"approved_at"' "${pack}/adds.json"; then echo "Error: adds.json does not record the approval" >&2 exit 1 fi echo "== replaying what only an approved photo can answer" "$PYTHON" "$REPO_ROOT/tool/e2e_media.py" --upload "$FAKE_URL" >/dev/null replay "$REPO_ROOT"/fake_signal/approved/*.yaml } enforce_the_time_budget() { local started="$1" local budget="${E2E_BUDGET_SECONDS:-40}" local elapsed=$(( $(date +%s) - started )) echo "== end to end took ${elapsed}s (budget ${budget}s)" if ((elapsed > budget)); then echo "Error: the offline run took ${elapsed}s, over its ${budget}s budget." >&2 echo " Either the new check earns its seconds and the budget moves," >&2 echo " or something got slower and this is the report of it." >&2 exit 1 fi }