mirror of
https://github.com/kuhyx/signal-bot.git
synced 2026-11-10 17:57:02 +01:00
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>
202 lines
8.9 KiB
Bash
Executable File
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
|
|
}
|