A second, self-contained client for the same hub, mobile-first: only
the sources about living (bodies, births, weather, sun & moon, play),
eight one-tap Vibes instead of a matrix, two macro knobs (ENERGY /
DREAM), a breathing orb, and dopamine Moments (somewhere a baby, the
sun where you stand, a kickoff). Its own compact Web Audio synth, all
routing client-side so it plays against the read-only public hub.
Reuses the invite-only /api auth; local sensors (heart BT, device tilt,
shake, mic, tap) never touch the hub. No money, no machinery, no
cables. Links back to the full instrument; added a "🌱 lifestrument"
item to godstrument's sky menu. Built understand→build→review with an
adversarial pass; 21 findings fixed (a11y sliders, zoom, contrast,
warm-dawn background, reachable heart connect, pluck de-click,
scheduler catch-up cap, moment tuning). test_lifestrument.py guards the
whitelist and the money/machinery ban.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
118 lines
4.1 KiB
Python
118 lines
4.1 KiB
Python
#!/usr/bin/env python3
|
|
"""Self-check for viz/life.html (Lifestrument). Stdlib only, no deps.
|
|
|
|
Asserts the three things that keep the fun page honest:
|
|
1. life.html parses as balanced HTML (well-formed tags).
|
|
2. every hub source it wires is a real config.json signal (or a local me.* one).
|
|
3. the banned families (money & machinery) appear NOWHERE in the file.
|
|
|
|
Run: python3 test_lifestrument.py
|
|
"""
|
|
import json
|
|
import os
|
|
import re
|
|
import sys
|
|
from html.parser import HTMLParser
|
|
|
|
HERE = os.path.dirname(os.path.abspath(__file__))
|
|
LIFE = os.path.join(HERE, "viz", "life.html")
|
|
CONFIG = os.path.join(HERE, "config.json")
|
|
|
|
BANNED_PREFIXES = ("crypto.", "fx.", "market.", "econ.", "debt.", "quake.", "planes.", "iss.")
|
|
# void elements never need a close tag
|
|
VOID = {"area", "base", "br", "col", "embed", "hr", "img", "input", "link",
|
|
"meta", "param", "source", "track", "wbr"}
|
|
|
|
|
|
class Balance(HTMLParser):
|
|
def __init__(self):
|
|
super().__init__()
|
|
self.stack = []
|
|
self.errors = []
|
|
|
|
def handle_starttag(self, tag, attrs):
|
|
if tag not in VOID:
|
|
self.stack.append(tag)
|
|
|
|
def handle_startendtag(self, tag, attrs):
|
|
pass # self-closing, balanced by definition
|
|
|
|
def handle_endtag(self, tag):
|
|
if tag in VOID:
|
|
return
|
|
if not self.stack:
|
|
self.errors.append(f"stray </{tag}>")
|
|
return
|
|
if self.stack[-1] == tag:
|
|
self.stack.pop()
|
|
elif tag in self.stack:
|
|
# close intervening (lenient, like a browser) but flag it
|
|
while self.stack and self.stack[-1] != tag:
|
|
self.stack.pop()
|
|
if self.stack:
|
|
self.stack.pop()
|
|
else:
|
|
self.errors.append(f"unexpected </{tag}>")
|
|
|
|
|
|
def test_balanced_html(html):
|
|
p = Balance()
|
|
p.feed(html)
|
|
assert not p.errors, f"HTML tag errors: {p.errors}"
|
|
assert not p.stack, f"unclosed tags: {p.stack}"
|
|
print(" ok: HTML is balanced")
|
|
|
|
|
|
def test_no_banned(html):
|
|
hits = []
|
|
for pref in BANNED_PREFIXES:
|
|
# word-ish boundary so 'econ.' doesn't match e.g. 'second.' — require a
|
|
# quote/backtick/space before the prefix, as our source keys always are.
|
|
for m in re.finditer(r'["\'`\s(]' + re.escape(pref), html):
|
|
frag = html[m.start():m.start() + 40].replace("\n", " ")
|
|
hits.append(f"{pref!r} near: …{frag}…")
|
|
assert not hits, "banned money/machinery keys present:\n " + "\n ".join(hits)
|
|
print(f" ok: none of {BANNED_PREFIXES} appear")
|
|
|
|
|
|
def test_whitelist_valid(html):
|
|
signals = set(json.load(open(CONFIG))["signals"].keys())
|
|
# pull the WHITELIST Set literal from the source
|
|
m = re.search(r"const WHITELIST = new Set\(\[(.*?)\]\)", html, re.S)
|
|
assert m, "could not find WHITELIST Set in life.html"
|
|
keys = re.findall(r'"([^"]+)"', m.group(1))
|
|
assert len(keys) >= 20, f"whitelist suspiciously small: {len(keys)} keys"
|
|
missing = [k for k in keys if k not in signals]
|
|
assert not missing, f"whitelist keys not in config.json signals: {missing}"
|
|
# none of the whitelisted keys may themselves be banned
|
|
bad = [k for k in keys if k.startswith(BANNED_PREFIXES)]
|
|
assert not bad, f"whitelist contains banned keys: {bad}"
|
|
print(f" ok: all {len(keys)} whitelist keys exist in config.json, none banned")
|
|
|
|
|
|
def test_reuses_real_endpoints(html):
|
|
for ep in ("/api/login", "/api/signup", "/api/me", "/api/logout"):
|
|
assert ep in html, f"missing expected auth endpoint {ep}"
|
|
# exact field names the hub expects
|
|
assert "identifier:" in html, "login must send {identifier, password}"
|
|
assert "invite:" in html, "signup must send an invite code"
|
|
print(" ok: reuses the real /api/* auth endpoints with correct fields")
|
|
|
|
|
|
def main():
|
|
html = open(LIFE, encoding="utf-8").read()
|
|
print("test_lifestrument.py — checking viz/life.html")
|
|
test_balanced_html(html)
|
|
test_no_banned(html)
|
|
test_whitelist_valid(html)
|
|
test_reuses_real_endpoints(html)
|
|
print("ALL CHECKS PASSED")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
try:
|
|
main()
|
|
except AssertionError as e:
|
|
print("FAIL:", e)
|
|
sys.exit(1)
|