Files
SocialPub/tools/pasture/town/selftest.py
T
thepraandClaude Opus 5.5 2873344690 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
2026-10-05 00:32:54 +02:00

155 lines
6.2 KiB
Python

"""Each driver against its own server, before any federation is trusted: two accounts of the platform make, like,
boost, reply to, vote on, edit and delete objects, and the database view (`stored`) must agree with what was done and
with the API. A failing selftest means the driver is wrong, not the federation."""
import time
import traceback
from dialects import driver
from dialects.base import Account, PostSpec, Unsupported
import state
PASSWORD = "Town-Selftest-Pass-7!"
def wait(fn, tries=20, delay=1.0):
for _ in range(tries):
value = fn()
if value:
return value
time.sleep(delay)
return fn()
class Result:
def __init__(self, peer):
self.peer = peer
self.passed = 0
self.failed = 0
self.skipped = 0
def check(self, cond, what):
if cond:
self.passed += 1
print(f" ok {what}")
else:
self.failed += 1
print(f" FAIL {what}")
def skip(self, what):
self.skipped += 1
print(f" n/a {what}")
def accounts(peer):
root = "st_root" if peer == "privapub" else None
# Mastodon reserves names that contain its own, so the platform goes in as its two-letter code
import gen
short = gen.SHORT.get(peer, peer[:2])
return [Account(peer, f"st_a_{short}", PASSWORD, name="Selftest A", bio="selftest", root=root,
fields=[("site", "pasture")], avatar_seed=f"st-a-{peer}"),
Account(peer, f"st_b_{short}", PASSWORD, name="Selftest B", root=root)]
def run(peers):
total_failed = 0
for peer in peers:
print(peer)
r = Result(peer)
try:
if peer == "lemmy":
lemmy(r)
else:
microblog(peer, r)
except Exception as e:
r.check(False, f"selftest stopped: {e!r}")
traceback.print_exc()
print(f" {r.passed} passed, {r.failed} failed, {r.skipped} not applicable")
total_failed += r.failed
return 1 if total_failed else 0
def microblog(peer, r):
d = driver(peer)
a, b = state.provision(peer, accounts(peer))
r.check(a.token and b.token, "both accounts signed in")
try:
d.update_profile(a, a.account)
r.check(True, "profile updated (name, bio, fields, avatar)")
except Unsupported as e:
r.skip(str(e))
made = d.post(a, PostSpec(text=f"selftest post on {peer} (st1)", tags=["townselftest"]))
row = wait(lambda: d.stored([made.uri])[made.uri].exists and d.stored([made.uri])[made.uri])
r.check(row and row.exists, f"the post is in the database under its uri {made.uri}")
r.check(row and str(row.local_id) == str(made.local_id), f"database id {row and row.local_id} is the API id {made.local_id}")
r.check(row and row.visibility == "public", f"stored visibility public (got {row and row.visibility})")
d.like(b, made.uri)
r.check(wait(lambda: d.stored([made.uri])[made.uri].likes == 1), "a like counts once")
try:
d.boost(b, made.uri)
r.check(wait(lambda: d.stored([made.uri])[made.uri].boosts == 1), "a boost counts once")
except Unsupported as e:
r.skip(str(e))
reply = d.post(b, PostSpec(text=f"selftest reply on {peer} (st2)", reply_to_uri=made.uri))
rrow = wait(lambda: d.stored([reply.uri])[reply.uri].exists and d.stored([reply.uri])[reply.uri])
r.check(rrow and rrow.parent_uri == made.uri, f"the reply names its parent (got {rrow and rrow.parent_uri})")
r.check(wait(lambda: (d.stored([made.uri])[made.uri].replies or 0) >= 1), "the parent counts the reply")
poll = d.post(a, PostSpec(text=f"selftest poll on {peer} (st3)", poll={"options": ["tea", "coffee"], "expires_in": 86400}))
d.vote(b, poll.uri, [1])
r.check(wait(lambda: (d.stored([poll.uri])[poll.uri].votes or [None, None])[1] == 1),
f"a vote counts on the second option (got {d.stored([poll.uri])[poll.uri].votes})")
private = d.post(a, PostSpec(text=f"selftest followers-only on {peer} (st4)", visibility="followers"))
prow = d.stored([private.uri])[private.uri]
r.check(prow.visibility == "followers", f"stored visibility followers (got {prow.visibility})")
seen = d.seen(b, [made.uri, private.uri])
r.check(seen[made.uri], "a non-follower sees the public post")
r.check(not seen[private.uri], "a non-follower does not see the followers-only post")
try:
d.edit(a, made.uri, PostSpec(text=f"selftest post on {peer}, edited (st1)"))
r.check(wait(lambda: d.stored([made.uri])[made.uri].edited), "the edit is recorded")
except Unsupported as e:
r.skip(str(e))
d.delete(b, reply.uri)
gone = wait(lambda: (lambda x: not x.exists or x.deleted)(d.stored([reply.uri])[reply.uri]))
r.check(gone, "a deleted reply is gone or tombstoned")
try:
d.react(b, made.uri, "🎉")
r.check(wait(lambda: (d.stored([made.uri])[made.uri].reactions or {}).get("🎉", 0) >= 1), "a reaction is stored")
except Unsupported as e:
r.skip(str(e))
def lemmy(r):
d = driver("lemmy")
a, b = state.provision("lemmy", accounts("lemmy"))
r.check(a.token and b.token, "both accounts signed in")
name = f"stc{int(time.time())}"
c = d.community(a, name, "Selftest community")
thread = d.post(a, PostSpec(text="selftest thread body (st1)", title="Selftest thread", group=c["id"]))
row = d.stored([thread.uri])[thread.uri]
r.check(row.exists and row.local_id == thread.local_id, f"the thread is in the database ({row.local_id})")
d.like(b, thread.uri)
r.check(wait(lambda: d.stored([thread.uri])[thread.uri].likes >= 2), "an upvote counts (with the author's own)")
comment = d.post(b, PostSpec(text="selftest comment (st2)", reply_to_uri=thread.uri))
crow = d.stored([comment.uri])[comment.uri]
r.check(crow.parent_uri == thread.uri, f"the comment names its thread (got {crow.parent_uri})")
reply = d.post(a, PostSpec(text="selftest comment reply (st3)", reply_to_uri=comment.uri))
rrow = d.stored([reply.uri])[reply.uri]
r.check(rrow.parent_uri == comment.uri, f"a nested comment names its parent comment (got {rrow.parent_uri})")
d.downvote(a, comment.uri)
r.check(wait(lambda: (d.stored([comment.uri])[comment.uri].reactions or {}).get("down") == 1), "a downvote counts")
d.edit(a, thread.uri, PostSpec(text="selftest thread body, edited (st1)"))
r.check(wait(lambda: d.stored([thread.uri])[thread.uri].edited), "the edit is recorded")
pm = d.post(a, PostSpec(text="selftest private message (st4)", visibility="direct",
mentions=[f"{b.account.username}@lemmy.test"]))
r.check(d.stored([pm.uri])[pm.uri].exists, "a private message is in the database")
d.delete(a, reply.uri)
r.check(wait(lambda: d.stored([reply.uri])[reply.uri].deleted), "a deleted comment is marked deleted")