signal-bot/scripts/e2e_proofs.sh
Krzysztof kuhy Rudnicki da016e1b94
Some checks failed
CI / check (push) Has been cancelled
CI / web (push) Has been cancelled
!dodaj lands in a queue; the management page approves it into the album
A photo sent to the chat no longer enters the album by itself. pack_add
strips it, checks it against the album and the queue, and writes it to
pending/ with a sidecar naming the people and labels the sender proposed;
the reply is "czeka na zatwierdzenie". The management page lists the queue
above the tagger, with the proposal pre-ticked, and Approve moves the file
in, mints the labels, records approved_at and asks the bot to rebuild its
table through /_reload -- the same publish path every other data change
takes, so PackHolder.reload is gone. A reload the bot refuses is reported
on the page, not rolled back: the photo is in the album either way.

The personal-data gate now reads pending/*.json too, so a label that is
still waiting for approval is forbidden in the tree already. The e2e run
boots the management page beside the bot with the same environment,
proves the queue is stripped and never reaches media/, approves it through
the real routes, and has !losowe answer with the photo afterwards without
a restart; exiftool is found through media_strip.locate() there rather
than PATH, which lacks vendor_perl on Arch.

!gadaj is dropped from !odcisz: it was within a typo of !dodaj, and the
nearest-command suggestion must never be a coin toss.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-04 22:55:39 +02:00

202 lines
8.9 KiB
Bash
Executable File

#!/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
}