signal-bot/scripts/live_smoke.sh
Krzysztof kuhy Rudnicki 0c42a6367c
Some checks are pending
CI / check (push) Waiting to run
feat: make the bot actually reply on Signal, and gate every way it went silent
First live reply, as its own registered account. `!cat` and `!dog` from the
phone return images -- verified in the production shape (the bot *invited to
an already-existing group*) and under a systemd user unit.

Four failures, each of which left the bot looking perfectly healthy:

- Invited is not joined. Signal only adds an account outright if the adder
  holds its profile key; with no profile the add becomes a pending invite,
  and signal-cli cannot accept one addressed to a phone number ("Cannot find
  service ID for self"). Group listed, websocket up, /healthz OK, zero
  messages. register_bot.sh now sets the profile *before* the add, and
  startup refuses on `member: false`.
- MODE=json-rpc-native is a GraalVM build with no AWT: text sends work,
  every attachment 400s with "Can't load library: awt". Both mode guards now
  demand exactly json-rpc, and start_container replaces a wrong-mode
  container instead of reusing it.
- The API spells one group two ways -- /v1/groups returns `group.<base64>`,
  envelopes carry what that decodes to -- so the scoping gate rejected the
  bot's own group. e2e.sh now models both spellings; reverting the fix fails
  4 scenarios, which is why the offline suite missed it before.
- BIND_PORT in .env was dead: __main__ read os.getenv before anything loaded
  a .env. The bind address comes from Config now.

A taken port used to be uvicorn logging the collision and exiting 0, which
every supervisor reads as success. portcheck.py refuses to start, exits
non-zero, and names the holder by reading /proc directly -- no subprocess, no
PATH assumptions, testable against a fake proc tree. Default port is 8137:
not 8000, and below the ephemeral range so the kernel cannot take it.

The bot now runs as signal-bot.service (Restart=always, linger checked rather
than assumed, StartLimitBurst so a config error fails visibly instead of
looping). Verified by SIGKILL and by re-running a command under the unit.

100% branch coverage, 230 tests, all gates green.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013yaBT45KhHqxTKVCTA87iy
2026-08-29 14:37:27 +02:00

122 lines
4.2 KiB
Bash
Executable File

#!/bin/bash
# ============================================================================
# Live verification against a real Signal account.
#
# Starts the signal-cli-rest-api container, opens the linking QR code, waits
# for the link, lists your groups so you can pick a private one, writes .env
# and starts the bot. The only manual step is scanning the QR code.
#
# Run scripts/e2e.sh first: this costs a device link, that costs nothing.
# ============================================================================
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
readonly SCRIPT_DIR
REPO_ROOT="$(dirname "$SCRIPT_DIR")"
readonly REPO_ROOT
readonly PYTHON="${PYTHON:-$REPO_ROOT/.venv/bin/python}"
readonly CONTAINER="${CONTAINER:-signal-api}"
readonly PORT="${PORT:-9922}"
readonly API="${API:-http://localhost:${PORT}}"
readonly DATA_DIR="${DATA_DIR:-$HOME/signal-api}"
# Stable tag. The dev series counts separately, so 0.167-dev is OLDER than this.
readonly IMAGE="${IMAGE:-bbernhard/signal-cli-rest-api:0.100}"
# Fallback when linking fails on client deprecation: tracks a newer signal-cli.
readonly FALLBACK_IMAGE="${FALLBACK_IMAGE:-bbernhard/signal-cli-rest-api:1787779383-ci}"
TEMP_DIR=""
# shellcheck source-path=SCRIPTDIR source=scripts/lib_signal.sh
. "${SCRIPT_DIR}/lib_signal.sh"
cleanup() {
if [[ -n "$TEMP_DIR" && -d "$TEMP_DIR" ]]; then
rm -rf "$TEMP_DIR"
fi
}
trap cleanup EXIT
validate_requirements() {
command -v docker >/dev/null || { echo "Error: docker is required" >&2; exit 1; }
command -v curl >/dev/null || { echo "Error: curl is required" >&2; exit 1; }
command -v jq >/dev/null || { echo "Error: jq is required" >&2; exit 1; }
[[ -x "$PYTHON" ]] || {
echo "Error: no interpreter at $PYTHON" >&2
echo " Run: uv venv .venv && uv pip install -e '.[dev]'" >&2
exit 1
}
}
start_container() {
local image="$1"
docker rm -f "$CONTAINER" >/dev/null 2>&1 || true
mkdir -p "$DATA_DIR"
# MODE=json-rpc, not json-rpc-native. Both expose the receive
# WebSocket, but the -native mode runs a GraalVM native-image build
# with no AWT, so every attachment send fails with
# "Can't load library: awt (UnsatisfiedLinkError)" -- meaning !cat
# and !dog return nothing while text replies work. Verified live on
# 2026-08-29: identical request, HTTP 400 native vs 201 on the JVM.
echo "== starting $image on :${PORT} (MODE=json-rpc)"
docker run -d --name "$CONTAINER" --restart=always -p "${PORT}:8080" \
-v "${DATA_DIR}:/home/.local/share/signal-cli" \
-e 'MODE=json-rpc' "$image" >/dev/null
if ! signal_wait_for_url "${API}/v1/about" "signal-cli-rest-api"; then
docker logs --tail 30 "$CONTAINER" >&2
return 1
fi
}
show_qr_code() {
local png="${TEMP_DIR}/link.png"
curl -sf "${API}/v1/qrcodelink?device_name=${CONTAINER}" -o "$png"
echo "== scan this QR code: Signal > Settings > Linked devices > +"
# The endpoint returns a PNG, so it has to be opened, not printed.
if command -v xdg-open >/dev/null; then
xdg-open "$png" >/dev/null 2>&1 &
else
echo " open this file: $png"
fi
}
wait_for_link() {
echo "== waiting for the link (Ctrl-C to abort)"
local attempt=0
until [[ "$(curl -sf "${API}/v1/accounts" | jq -r 'length // 0')" != "0" ]]; do
attempt=$((attempt + 1))
if [[ $attempt -ge 300 ]]; then
echo "Error: no account linked after 5 minutes." >&2
echo " If linking failed with a connection error, retry with:" >&2
echo " IMAGE=$FALLBACK_IMAGE $0" >&2
exit 1
fi
sleep 1
done
}
main() {
validate_requirements
TEMP_DIR="$(mktemp -d)"
start_container "$IMAGE"
signal_verify_mode
show_qr_code
wait_for_link
local number group
number="$(curl -sf "${API}/v1/accounts" | jq -r '.[0]')"
echo "== linked as ${number}"
group="$(signal_choose_group "$number")"
[[ -n "$group" && "$group" != "null" ]] || { echo "Error: no group chosen" >&2; exit 1; }
signal_write_env "$REPO_ROOT" "$number" "$group"
echo "== starting the bot. Send '!cat' in that group; you should get a cat."
exec "$REPO_ROOT/run.sh"
}
main "$@"