🌱 LIFESTRUMENT — the gentle client (viz/life.html)
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>
This commit is contained in:
parent
dab04294ef
commit
350ad72782
117
test_lifestrument.py
Normal file
117
test_lifestrument.py
Normal file
@ -0,0 +1,117 @@
|
||||
#!/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)
|
||||
@ -6642,6 +6642,7 @@
|
||||
: "🌐 tab feed cancelled";
|
||||
eventTicker.unshift({ text: msg, tAdded: now(), src: "sys" }); });
|
||||
ctxItem("⌨ keys — remap the keyboard", () => openKeys());
|
||||
ctxItem("🌱 lifestrument — the gentle one (new tab)", () => window.open("/life.html", "_blank"));
|
||||
if (commune.room) {
|
||||
const gOwner = commune.alloc["@godtime"];
|
||||
ctxItem(gOwner === communeMe() ? "🕸 release the godtime" :
|
||||
|
||||
1151
viz/life.html
Normal file
1151
viz/life.html
Normal file
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user