194 lines
7.2 KiB
Bash
Executable file
194 lines
7.2 KiB
Bash
Executable file
#!/usr/bin/env bash
|
|
# smolboid integration rig (PLAN.md step 7): a local bunshin, the fumi CLI as
|
|
# the author, and the daemon under test — covering post, command, repost,
|
|
# timestamp collision, size-cap warning, both prune modes and an author
|
|
# rotation. Real binaries, real Noise sessions, real envelopes.
|
|
set -euo pipefail
|
|
|
|
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
|
FUMI_DIR="$ROOT/../fumi"
|
|
BUNSHIN_DIR="$ROOT/../bunshin"
|
|
RIG="$(mktemp -d /tmp/smolboid-rig.XXXXXX)"
|
|
HOST=127.0.0.1
|
|
SMTP_PORT=19611
|
|
HTTP_PORT=18081
|
|
DAEMON_ADDR="twtxt@$HOST:$SMTP_PORT"
|
|
AUTHOR_ADDR="tobias@$HOST:$SMTP_PORT"
|
|
DATA="$RIG/data"
|
|
|
|
BUNSHIN_PID=""
|
|
SMOLBOID_PID=""
|
|
cleanup() {
|
|
[[ -n "$BUNSHIN_PID" ]] && kill "$BUNSHIN_PID" 2>/dev/null || true
|
|
[[ -n "$SMOLBOID_PID" ]] && kill "$SMOLBOID_PID" 2>/dev/null || true
|
|
}
|
|
trap cleanup EXIT
|
|
|
|
step() { printf '\n== %s ==\n' "$*"; }
|
|
fail() { echo "FAIL: $*" >&2; exit 1; }
|
|
|
|
wait_for() { # file, text, [tries]
|
|
local file=$1 text=$2 tries=${3:-20} i
|
|
for ((i = 0; i < tries; i++)); do
|
|
[[ -f $file ]] && grep -q "$text" "$file" && return 0
|
|
sleep 1
|
|
done
|
|
fail "timed out waiting for '$text' in $file"
|
|
}
|
|
|
|
wait_gone() { # file, text, [tries]
|
|
local file=$1 text=$2 tries=${3:-20} i
|
|
for ((i = 0; i < tries; i++)); do
|
|
grep -q "$text" "$file" || return 0
|
|
sleep 1
|
|
done
|
|
fail "'$text' is still in $file"
|
|
}
|
|
|
|
step "build bunshin, fumi, smolboid"
|
|
cargo build --manifest-path "$BUNSHIN_DIR/Cargo.toml" --quiet
|
|
cargo build --manifest-path "$FUMI_DIR/Cargo.toml" --quiet
|
|
cargo build --manifest-path "$ROOT/Cargo.toml" --quiet
|
|
BUNSHIN="$BUNSHIN_DIR/target/debug/bunshin"
|
|
FUMI="$FUMI_DIR/target/debug/fumi"
|
|
SMOLBOID="$ROOT/target/debug/smolboid"
|
|
|
|
step "bunshin on $HOST:$SMTP_PORT"
|
|
"$BUNSHIN" keygen --key "$RIG/server.key" >"$RIG/keygen.log" 2>&1
|
|
SERVER_KEY="$(grep -oE '[a-z2-7]{52}' "$RIG/keygen.log" | head -1)"
|
|
[[ -n "$SERVER_KEY" ]] || { cat "$RIG/keygen.log"; fail "no server key"; }
|
|
"$BUNSHIN" serve --key "$RIG/server.key" --db "$RIG/server.db" \
|
|
--host "$HOST" --port "$SMTP_PORT" >"$RIG/bunshin.log" 2>&1 &
|
|
BUNSHIN_PID=$!
|
|
sleep 1
|
|
|
|
step "identities: author tobias, daemon twtxt"
|
|
mkdir -p "$DATA"
|
|
fumi() { "$FUMI" --key "$RIG/author.key" --db "$RIG/author.db" --timeout 10 "$@"; }
|
|
dfumi() { "$FUMI" --key "$RIG/master.key" --db "$DATA/mail.db" --timeout 10 "$@"; }
|
|
fumi keygen --force >/dev/null
|
|
fumi trust "$HOST:$SMTP_PORT" "$SERVER_KEY"
|
|
fumi register "$AUTHOR_ADDR" >/dev/null
|
|
"$SMOLBOID" keygen "$RIG/master.key" >/dev/null
|
|
dfumi trust "$HOST:$SMTP_PORT" "$SERVER_KEY"
|
|
dfumi register "$DAEMON_ADDR" >/dev/null
|
|
|
|
step "admit tobias"
|
|
cat >"$RIG/smolboid.toml" <<EOF
|
|
listen = $HOST:$HTTP_PORT
|
|
base_url = http://$HOST:$HTTP_PORT
|
|
smolmail_host = $HOST
|
|
mailbox = twtxt
|
|
master = $RIG/master.key
|
|
data = $DATA
|
|
poll_secs = 1
|
|
timeout = 10
|
|
cap_bytes = 1048576
|
|
EOF
|
|
"$SMOLBOID" admit --config "$RIG/smolboid.toml" "$AUTHOR_ADDR" >"$RIG/admit.log" 2>&1 \
|
|
|| { cat "$RIG/admit.log"; fail "admit"; }
|
|
grep -q "welcome mail sent" "$RIG/admit.log" \
|
|
|| { cat "$RIG/admit.log"; fail "no welcome mail"; }
|
|
# The Accept token's proof is the next step: without it, posts stay in the
|
|
# requests tier and nothing ever publishes.
|
|
fumi fetch >/dev/null
|
|
FEED="$DATA/feeds/tobias.txt"
|
|
|
|
step "serve"
|
|
"$SMOLBOID" serve --config "$RIG/smolboid.toml" >"$RIG/serve.log" 2>&1 &
|
|
SMOLBOID_PID=$!
|
|
sleep 2
|
|
|
|
step "post"
|
|
fumi send "$DAEMON_ADDR" --body "hello smolverse" >/dev/null
|
|
wait_for "$FEED" "hello smolverse"
|
|
"$SMOLBOID" check "$FEED" >"$RIG/check.log" || { cat "$RIG/check.log"; fail "check"; }
|
|
grep -q "1 verified, 0 unsigned, 0 invalid" "$RIG/check.log" \
|
|
|| { cat "$RIG/check.log"; fail "the first post did not verify"; }
|
|
curl -sf "http://$HOST:$HTTP_PORT/twtxt/tobias.txt" | grep -q "hello smolverse" \
|
|
|| fail "the feed is not served over HTTP"
|
|
[[ "$(curl -s -o /dev/null -w '%{http_code}' "http://$HOST:$HTTP_PORT/twtxt/nobody.txt")" == 404 ]] \
|
|
|| fail "a missing feed must 404"
|
|
|
|
step "commands: nick, repost"
|
|
fumi send "$DAEMON_ADDR" --header "Twt-Nick: toby" --body "" >/dev/null
|
|
wait_for "$FEED" "# nick = toby"
|
|
fumi send "$DAEMON_ADDR" \
|
|
--header "Twt-Repost: @<tobias http://$HOST:$HTTP_PORT/twtxt/tobias.txt> #abc1234" \
|
|
--body "" >/dev/null
|
|
wait_for "$FEED" "# smol:repost .* #abc1234"
|
|
"$SMOLBOID" check "$FEED" >"$RIG/check.log" || { cat "$RIG/check.log"; fail "check"; }
|
|
grep -q "2 verified, 0 unsigned, 0 invalid" "$RIG/check.log" \
|
|
|| { cat "$RIG/check.log"; fail "the repost did not verify"; }
|
|
|
|
step "delete"
|
|
TS="$(grep "hello smolverse" "$FEED" | cut -f1)"
|
|
fumi send "$DAEMON_ADDR" --header "Twt-Delete: $TS" --body "" >/dev/null
|
|
wait_gone "$FEED" "hello smolverse"
|
|
|
|
step "timestamp collision"
|
|
collided=0
|
|
for i in 1 2 3 4 5; do
|
|
fumi send "$DAEMON_ADDR" --body "collide A $i" >/dev/null &
|
|
A_PID=$!
|
|
fumi send "$DAEMON_ADDR" --body "collide B $i" >/dev/null &
|
|
B_PID=$!
|
|
# Only the two sends, never the servers.
|
|
wait "$A_PID" "$B_PID"
|
|
sleep 3
|
|
if grep -q "collide A $i" "$FEED" && grep -q "collide B $i" "$FEED"; then
|
|
continue # different seconds; try again
|
|
fi
|
|
fumi fetch >/dev/null
|
|
if fumi list --requests 2>/dev/null | grep -q "timestamp .* already exists"; then
|
|
collided=1
|
|
break
|
|
fi
|
|
done
|
|
[[ "$collided" == 1 ]] || fail "no collision rejection arrived after 5 attempts"
|
|
|
|
step "retention: hard limit"
|
|
kill "$SMOLBOID_PID" 2>/dev/null || true
|
|
# Metadata (~190B) plus one record (~253B) must fit under the cap, and land
|
|
# above its 90% mark so the warning fires.
|
|
sed -i 's/^cap_bytes = .*/cap_bytes = 460/' "$RIG/smolboid.toml"
|
|
"$SMOLBOID" serve --config "$RIG/smolboid.toml" >>"$RIG/serve.log" 2>&1 &
|
|
SMOLBOID_PID=$!
|
|
sleep 2
|
|
fumi send "$DAEMON_ADDR" --body "over the cap" >/dev/null
|
|
sleep 3
|
|
grep -q "over the cap" "$FEED" && fail "a capped feed published a record"
|
|
fumi fetch >/dev/null
|
|
fumi list --requests 2>/dev/null | grep -q "feed is at its size cap" \
|
|
|| fail "no cap rejection reply"
|
|
|
|
step "retention: fifo mode"
|
|
fumi send "$DAEMON_ADDR" --header "Twt-Prune: fifo" --body "" >/dev/null
|
|
wait_for "$DATA/state.json" '"prune": "Fifo"'
|
|
fumi send "$DAEMON_ADDR" --body "fifo post" >/dev/null
|
|
wait_for "$FEED" "fifo post"
|
|
[[ "$(grep -cE '^[0-9]' "$FEED")" == 1 ]] || fail "fifo did not prune down to one record"
|
|
fumi fetch >/dev/null
|
|
fumi list --requests 2>/dev/null | grep -q "90% of its size cap" \
|
|
|| fail "no 90% warning reply"
|
|
"$SMOLBOID" check "$FEED" >"$RIG/check.log" || { cat "$RIG/check.log"; fail "check"; }
|
|
grep -q "1 verified, 0 unsigned, 0 invalid" "$RIG/check.log" \
|
|
|| { cat "$RIG/check.log"; fail "the pruned feed does not verify"; }
|
|
|
|
step "author rotation"
|
|
fumi rotate >/dev/null
|
|
fumi send "$DAEMON_ADDR" --body "after rotation" >/dev/null
|
|
wait_for "$FEED" "after rotation"
|
|
OWNERS="$(python3 -c "import json;print(len(json.load(open('$DATA/state.json'))['feeds'][0]['owner_keys']))")"
|
|
[[ "$OWNERS" == 2 ]] || fail "expected 2 owner keys after rotation, got $OWNERS"
|
|
"$SMOLBOID" check "$FEED" >"$RIG/check.log" || { cat "$RIG/check.log"; fail "check"; }
|
|
# Under the fifo cap only one record fits, so the rotation post pruned the
|
|
# fifo post; what remains must fully verify.
|
|
grep -q "1 verified, 0 unsigned, 0 invalid" "$RIG/check.log" \
|
|
|| { cat "$RIG/check.log"; fail "rotation-era records do not verify"; }
|
|
|
|
step "rig passed"
|
|
echo "feeds: $FEED"
|
|
echo "logs: $RIG"
|
|
cleanup
|
|
trap - EXIT
|