The town: a fake community across the pasture, checked for coherence

tools/pasture/town/ (run through tools/pasture/town.sh) seeds a fake
community across every running peer and checks that all of them, and
PrivaPub, agree on what happened:

- drivers per platform on four dialect bases (Mastodon API, Misskey API,
  Lemmy API, PrivaPub with /clientapi), each with a selftest against
  its own server; what a server holds is read from its database, never
  by making it fetch;
- a deterministic generator (specs/village.json: 23 accounts on seven
  servers, roots with several personas, circles and communities, a
  cross-server follow graph, posts of every kind and visibility, reply
  rounds, likes, boosts, reactions, votes, edits, deletes, blocks,
  mutes and a report) and a seeder that keeps a ledger of what happened;
- a sweep that expects delivery and confinement per server, what each
  account sees, counts, threads, edits, deletes, follows and privacy
  rows (sibling keys, published days, canary root credentials in every
  peer's database, located posts that never leave), with what the peers
  do on purpose modelled (Misskey drops orphan replies, Lemmy keeps only
  community content, edits go to the post's own audience);
- known gaps (gaps.json) turn failures into xfail and passes into xpass;
  a self-contained report.html, and docs/INTEROP-BACKLOG.md.

The pasture moves to a public-looking subnet (peers with no private
address switch can join), takes PASTURE_PORT when 6971 is in use, adds
peers to a running pasture (run.sh add, Caddy recreated with its CA
kept), removes its volumes on down, writes every scenario check to
out/scenarios.jsonl, serves decePub as decepub.test for its e2e tests,
lifts GoToSocial's and Lemmy's own rate limits, trusts Caddy in
Mastodon (TRUSTED_PROXY_IP) and gains Hollo (Fedify), whose one login
owning several accounts is the nearest peer to PrivaPub's personas.

The first village found the four PrivaPub bugs fixed in the commits
before this one; the second run, on the fixed server, passes 2319 checks
with 11 failures left, all between peers or from Lemmy's send worker,
which the seeder now warms up first. ROADMAP records the owner's
decisions of 2026-10-04 (the town, and P9 back from the cut list).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LsXgEaXee4GCU1hwYgPJXw
This commit is contained in:
thepraandClaude Opus 5.5 committed 2026-10-05 00:32:54 +02:00
1 parent d6131af289
commit 2873344690
46 files changed
+4400 -24

No files matched your search

+28
View File
@@ -0,0 +1,28 @@
# decePubClient as tests/e2e/run.sh publishes it (environment Pasture: its API is https://privapub.test), served like the
# production vhost (deploy/nginx in decePubClient): static files, the SPA fallback, nothing cached that must not be.
DECEPUB_SITE=${DECEPUB_SITE:-$repo/../decePubClient/tests/e2e/.publish/wwwroot}
decepub_up() {
[ -f "$DECEPUB_SITE/index.html" ] || { echo "no published decePub in $DECEPUB_SITE (decePubClient/tests/e2e/run.sh publishes it)" >&2; return 1; }
mkdir -p "$here/.state"
cat > "$here/.state/decepub.Caddyfile" <<'CADDY'
:80 {
root * /srv
header {
X-Content-Type-Options nosniff
Referrer-Policy strict-origin-when-cross-origin
X-Frame-Options SAMEORIGIN
}
@fresh path /index.html /appsettings.json /appsettings.Pasture.json /service-worker.js /service-worker-assets.js /build.json
header @fresh Cache-Control no-cache
try_files {path} /index.html
file_server {
precompressed br gzip
}
}
CADDY
podman run -d --replace --name pasture-decepub --network $net -v "$DECEPUB_SITE:/srv:z,ro" \
-v "$here/.state/decepub.Caddyfile:/etc/caddy/Caddyfile:z,ro" docker.io/library/caddy:2 >/dev/null
podman exec pasture-caddy caddy reload --config /etc/caddy/Caddyfile >/dev/null 2>&1 || true
echo "decepub: https://decepub.test:6443"
}
+1 -1
View File
@@ -7,7 +7,7 @@ gts_up() {
-e GTS_HOST=gts.test -e GTS_PROTOCOL=https -e GTS_PORT=80 -e GTS_BIND_ADDRESS=0.0.0.0 -e GTS_HTTP_CLIENT_TLS_INSECURE_SKIP_VERIFY=true \
-e GTS_DB_TYPE=sqlite -e GTS_DB_ADDRESS=/gotosocial/storage/sqlite.db -e GTS_STORAGE_LOCAL_BASE_PATH=/gotosocial/storage \
-e GTS_LETSENCRYPT_ENABLED=false -e GTS_HTTP_CLIENT_ALLOW_IPS=10.0.0.0/8,172.16.0.0/12,192.168.0.0/16 \
-e GTS_TRUSTED_PROXIES=0.0.0.0/0 -e GTS_LOG_LEVEL=info -e GTS_INSTANCE_EXPOSE_PUBLIC_TIMELINE=true \
-e GTS_TRUSTED_PROXIES=0.0.0.0/0 -e GTS_LOG_LEVEL=info -e GTS_ADVANCED_RATE_LIMIT_REQUESTS=0 -e GTS_INSTANCE_EXPOSE_PUBLIC_TIMELINE=true \
$GTS_IMAGE >/dev/null
wait_http http://127.0.0.1:6972/api/v1/instance
podman exec pasture-gts /gotosocial/gotosocial admin account create --username gtsuser --email gtsuser@gts.test --password 'Gts-Pasture-Pass-1!' >/dev/null 2>&1 || true
+26
View File
@@ -0,0 +1,26 @@
# Hollo 0.9 (Fedify): one login owning several accounts, the nearest thing to PrivaPub's personas. RFC 9421 signatures
# first (cavage after a refusal), FEP-8b32 proofs, FEP-521a keys. On the shared Postgres (database hollo). The town's
# driver makes the login and the accounts through its forms (dialects/hollo.py); ALLOW_PRIVATE_ADDRESS only matters if
# the pasture's subnet is made private again.
HOLLO_IMAGE=${HOLLO_IMAGE:-ghcr.io/fedify-dev/hollo:0.9.19}
. "$here/peers/shared.sh"
hollo_up() {
shared_postgres_up
pg_db hollo
mkdir -p "$here/.state/hollo/media" && chmod a+rwx "$here/.state/hollo/media"
podman run -d --replace --name pasture-hollo --network $net -v "$here/.state/hollo/media:/var/lib/hollo:z" \
-e DATABASE_URL=postgres://pasture:pasture@postgres:5432/hollo -e SECRET_KEY=pasture-hollo-not-a-secret-0123456789abcdefghijklmn \
-e BEHIND_PROXY=true -e ALLOW_PRIVATE_ADDRESS=true -e NODE_EXTRA_CA_CERTS=/pasture/ca/root.crt -e LOG_LEVEL=info \
-e DRIVE_DISK=fs -e FS_STORAGE_PATH=/var/lib/hollo -e STORAGE_URL_BASE=https://hollo.test/assets/ \
-v "$ca:/pasture/ca:z,ro" $HOLLO_IMAGE >/dev/null
for _ in $(seq 1 90); do
site hollo.test -s -o /dev/null -w '%{http_code}' https://hollo.test:6443/.well-known/nodeinfo 2>/dev/null | grep -q 200 && break
sleep 2
done
# the one login: its accounts are made by the town as it needs them
# its forms compare Origin with the Host, so the Host leaves out the workstation's port
site hollo.test -s -o /dev/null -X POST https://hollo.test:6443/setup -H 'Origin: https://hollo.test' -H 'Host: hollo.test' \
--data-urlencode email=owner@hollo.test --data-urlencode 'password=Hollo-Pasture-Pass-1' --data-urlencode 'password_confirm=Hollo-Pasture-Pass-1'
echo "hollo: https://hollo.test:6443"
}
+8
View File
@@ -29,8 +29,16 @@ HJSON
site lemmy.test -s -o /dev/null -w '%{http_code}' https://lemmy.test:6443/api/v4/site 2>/dev/null | grep -q 200 && break
sleep 2
done
# Lemmy's default limits (6 posts or comments per 10 minutes, 10 sign-ups an hour) would throttle a scripted town;
# Lemmy reads them again when its site is edited
podman exec pasture-postgres psql -U pasture -d lemmy -qc "update local_site_rate_limit set message_max_requests=100000,
message_interval_seconds=1, post_max_requests=100000, post_interval_seconds=1, register_max_requests=100000,
register_interval_seconds=1, image_max_requests=100000, image_interval_seconds=1, comment_max_requests=100000,
comment_interval_seconds=1, search_max_requests=100000, search_interval_seconds=1" >/dev/null
site lemmy.test -s -X POST https://lemmy.test:6443/api/v4/account/auth/login -H 'Content-Type: application/json' \
-d '{"username_or_email":"lemmyuser","password":"Lemmy-Pasture-Pass-1"}' \
| python3 -c "import sys,json; print(json.load(sys.stdin)['jwt'])" > "$here/.state/lemmy.token" 2>/dev/null || true
site lemmy.test -s -o /dev/null -X PUT https://lemmy.test:6443/api/v4/site -H "Authorization: Bearer $(cat "$here/.state/lemmy.token")" \
-H 'Content-Type: application/json' -d '{"registration_mode":"open","email_verification_required":false,"captcha_enabled":false}'
echo "lemmy: https://lemmy.test:6443"
}
+1
View File
@@ -18,6 +18,7 @@ ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY=pasturedeterministickey0000000000
ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT=pasturederivationsalt000000000000
ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY=pastureprimarykey0000000000000000
ALLOWED_PRIVATE_ADDRESSES=10.0.0.0/8,172.16.0.0/12,192.168.0.0/16
TRUSTED_PROXY_IP=$subnet
SSL_CERT_FILE=/pasture/ca/bundle.pem
SKIP_POST_DEPLOYMENT_MIGRATIONS=false
WEB_CONCURRENCY=0
+9
View File
@@ -11,3 +11,12 @@ shared_redis_up() {
podman container exists pasture-redis && return 0
podman run -d --replace --name pasture-redis --network $net --network-alias redis docker.io/library/redis:7-alpine >/dev/null
}
# pg_db <name> [extension...]: a database of the shared Postgres for one peer, with the extensions it needs
pg_db() {
local name=$1; shift
podman exec pasture-postgres sh -c "psql -U pasture -tc \"select 1 from pg_database where datname='$name'\" | grep -q 1 || createdb -U pasture $name"
for ext in "$@"; do
podman exec pasture-postgres psql -U pasture -d "$name" -qc "create extension if not exists \"$ext\";" >/dev/null
done
}