Files
felhom.eu/documentation/audits/usenet-2026-10-12/tools/boxstep_usenet.py
T
admin 52615d9139
gates / gates (push) Successful in 6m34s
Website + evidence: the Usenet pair (R-988 closed), and the drill route filed as R-989
The catalog published SABnzbd and NZBGet today (app-catalog 8ee687e). Here:

- two cards in the media category on both language sets, each with the project's own logo and
  each saying IN BOLD that a paid Usenet subscription is the household's own
- the published count 60 -> 62 everywhere it is stated; the category badges are derived from
  the cards now (media read 15 with 17 cards, otthon/home read 14 with 16 — both stale)
- documentation/audits/usenet-2026-10-12/: both bench runs, both box steps and verdicts, the
  nine defects with their measurements, what a missing provider left unmeasured, the teardown
- R-988 closed (moved to CLOSED-ITEMS in this commit); R-989 filed: the drill-catalog route of
  09 §6.5 cannot be followed — that repo is private and no box has catalog credentials, so the
  templates were staged in the box's own catalog cache instead
- walk.app_env: a SECRET field in app.yaml is stored ENC:, so that helper returns ciphertext —
  the docstring says so now, measured the hard way
- STATUS.md and REPORT-usenet-2026-10-12.md

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0117kRTSgs3rJWUFGxyoWN6Y
2026-10-12 22:58:59 +02:00

129 lines
5.1 KiB
Python

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""The BOX venue for the two Usenet clients: the walk, then the ladder's first step, in ONE process.
One process because the generated deploy values (the household's own password and API key) live in
`walk.GENERATED`, in memory - the box fixtures need them to seed and to read back, so a separate run
would have nothing to sign in with.
Per app:
1. the walk (boxwalk_usenet): install from the staged FROM pin, then the stranger/household probes
2. prep.txt - the pinned images the install is actually running
3. the box fixture SEEDS through the app's own interface, as the household
4. the staged template moves to the TO pin and the box rescans (this is what `drill_bump` does to
the drill repo; that repo is private and this box has no catalog credentials, see
stage_in_cache.py, so the same move is made in the box's own catalog cache)
5. the product's own guarded Update is PRESSED and every phase recorded
6. the box fixture READS BACK, with its negative control
7. box-verdict-<app>.json - `proven` only if the update ended `done`, the app is healthy, and the
readback says the data survived. That file is what `upgrade-test.py --write-ladder` consumes,
and it refuses to write a ladder entry without it.
Runs ON DOOPLEX.
"""
import json
import os
import sys
import time
HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, HERE)
TOOLS = "/mnt/5_hdd/felhom.eu/git/felhom.eu/documentation/audits/pg-last-six-2026-09-30/tools"
sys.path.insert(0, TOOLS)
sys.path.insert(0, "/mnt/5_hdd/felhom.eu/git/app-catalog-felhom.eu/scripts")
os.environ.setdefault("SC", "/tmp/usenet-walk")
import walk as w # noqa: E402
import boxwalk_usenet as bw # noqa: E402
import upgrade_fixtures_box as fxb # noqa: E402
import stage_in_cache as stage # noqa: E402
EV = bw.EV
PINS = stage.PINS
SUBS = {"sabnzbd": "usenet", "nzbget": "nzbget"}
def pinned(app):
st = w.stack(app)
return st.get("pinned_images") or st.get("images") or {}
def step(app):
sub = SUBS[app]
frm, to = PINS[app]
d = os.path.join(EV, app)
os.makedirs(d, exist_ok=True)
log = []
def say(line):
log.append(line)
w.say(line)
say(f"before: pinned={pinned(app)}")
fx = fxb.FIXTURES[app]
# 3 — seed as the household, through the app's own interface
seeded = fx.seed(w, sub, say)
if not seeded:
open(os.path.join(d, "step.txt"), "w").write("\n".join(log) + "\nSEED FAILED\n")
return {"app": app, "verdict": "inconclusive", "why": "the box fixture could not seed"}
# 4 — the catalog moves to the TO pin (staged, because the drill repo is unreachable)
stage.stage("to")
say(f"staged the catalog at the TO pin: {to}")
w.ctl("POST", "/api/stacks/rescan")
time.sleep(5)
say(f"catalog_images now: {w.stack(app).get('catalog_images')}")
# 5 — the product's own Update
res = w.press_update(app)
for ph in res.get("phases", []):
say(f" phase +{ph['t']}s {ph['phase']} | err={ph['error']}")
say(f"update accepted={res.get('accepted')} final={res.get('final_phase')} "
f"err={res.get('update_error')} {res.get('duration_s')}s")
# 6 — the readback, with the fixture's own negative control
ok = fx.verify(w, sub, seeded, say)
obs = w.observables(app)
say(f"observables: {json.dumps(obs)[:300]}")
say(f"after: pinned={pinned(app)}")
verdict = {
"app": app,
"venue": "box 9202 (templates staged in the box's catalog cache - the drill repo is private "
"and the box has no catalog credentials), the product's guarded Update",
"from": {app: frm},
"to": {app: to},
"verdict": "proven" if (ok and res.get("final_phase") == "done") else "inconclusive",
"seed_read_before": True,
"seed_read_after": bool(ok),
"healthy_after": bool(obs.get("health") in ("healthy", None) and obs.get("state") == "running")
if isinstance(obs, dict) else None,
"duration_s": res.get("duration_s"),
"final_phase": res.get("final_phase"),
"update_error": res.get("update_error"),
"measured_at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
"evidence": f"felhom.eu/documentation/audits/usenet-2026-10-12/box/{app}/step.txt",
}
open(os.path.join(d, "step.txt"), "w").write("\n".join(log) + "\n")
json.dump(verdict, open(os.path.join(d, f"box-verdict-{app}.json"), "w"), indent=2, ensure_ascii=False)
say(f"=== {app}: {verdict['verdict']} ===")
return verdict
def main():
w.login()
bw.main() # the walk: install + the stranger/household probes
out = {}
for app in ("sabnzbd", "nzbget"):
if (bw.OUT.get(app) or {}).get("deploy"):
out[app] = step(app)
else:
out[app] = {"app": app, "verdict": "not-attempted", "why": "the install did not succeed"}
json.dump(out, open(os.path.join(EV, "box-steps.json"), "w"), indent=2, ensure_ascii=False)
print(json.dumps(out, indent=2, ensure_ascii=False))
if __name__ == "__main__":
main()