harness: claper fixture — its own release CLI (rpc register_user / authenticate), proven on 9202
gates / gates (push) Successful in 2s

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
This commit is contained in:
2026-09-28 10:32:45 +02:00
parent f1a7d6c9fd
commit c5015ce255
+56
View File
@@ -376,6 +376,61 @@ class Django:
return found is True return found is True
# =============================================================================================
class Claper:
"""Claper's OWN release CLI inside its own container: `bin/claper rpc` runs Elixir code in the
RUNNING node, against the app's own `Claper.Accounts` context (its changeset, its password hash).
Not SQL, not a planted file (R-156). `eval` would boot a second, app-less VM — `rpc` asks the
live one. Measured on 9202 2026-09-28 (claper 2.5.1, PostgreSQL 16): register_user → {:ok, id=2};
the readback authenticated with the right password and REFUSED a wrong one.
THE FIXTURE PROVES ITSELF ON EVERY CALL: verify() also asks the same function with a password
that was never set and requires False — a readback that has broken into "always found" fails.
"""
container = "claper"
def _rpc(self, w, code):
# ELIXIR_ERL_OPTIONS=+fnu: the release otherwise warns about latin1 on every call (noise only).
out = w.guest(f"docker exec -e ELIXIR_ERL_OPTIONS=+fnu {self.container} /app/bin/claper rpc "
f"{json.dumps(code)} 2>&1", timeout=240)
return " ".join(out.split())
def _auth(self, w, email, pw):
out = self._rpc(w, f'IO.puts("DRILL_ANSWER=#{{Claper.Accounts.get_user_by_email_and_password({json.dumps(email)}, {json.dumps(pw)}) != nil}}")')
m = re.search(r"DRILL_ANSWER=(true|false)", out)
return (m.group(1) == "true") if m else None, out[-300:]
def seed(self, w, sub, say):
if not w.wait_app(sub, "/", want=("200", "302"), tries=90):
return None
email = f"drill-{secrets.token_hex(4)}@gate.invalid"
pw = "Drill-" + secrets.token_hex(10)
out = self._rpc(w, ('case Claper.Accounts.register_user(%{email: ' + json.dumps(email) + ', password: '
+ json.dumps(pw) + '}) do {:ok, u} -> IO.puts("DRILL_SEEDED=#{u.id}"); '
'{:error, cs} -> IO.inspect(cs.errors, label: "DRILL_REFUSED") end'))
say(f" claper: register_user :: {out[-160:]}")
got, detail = self._auth(w, email, pw)
if got is not True:
say(f" claper: the account does not authenticate in the app's own context :: {detail[:200]}")
return None
say(f" claper: seeded user {email}")
return {"email": email, "pw": pw}
def verify(self, w, sub, t, say):
if not w.wait_app(sub, "/", want=("200", "302"), tries=90):
say(" claper: the app never served /")
return False
wrong, detail = self._auth(w, t["email"], "definitely-" + secrets.token_hex(8))
if wrong is not False:
say(f" claper: READBACK UNUSABLE — a wrong password did not read as refused ({wrong}) :: {detail[:200]}")
return False
ok, detail = self._auth(w, t["email"], t["pw"])
say(f" claper: readback — the seeded account authenticates={ok}")
if ok is not True:
say(f" claper: :: {detail[:250]}")
return ok is True
# ============================================================================================= # =============================================================================================
class Nextcloud: class Nextcloud:
"""Nextcloud's OWN admin CLI, `occ`, inside its own container: its own code, its own user """Nextcloud's OWN admin CLI, `occ`, inside its own container: its own code, its own user
@@ -1089,4 +1144,5 @@ FIXTURES = {
"navidrome": Navidrome(), "navidrome": Navidrome(),
"vaultwarden": Vaultwarden(), "vaultwarden": Vaultwarden(),
"wishlist": Wishlist(), "wishlist": Wishlist(),
"claper": Claper(),
} }