clanker / tests /test_api.py
deucebucket's picture
feat: ship the redesigned pixel UI wired to the real engine (+ crisis/soul/distance API fields)
41cf16c verified
Raw
History Blame Contribute Delete
9.56 kB
from fastapi.testclient import TestClient
from app.main import app
client = TestClient(app)
def test_health_ok():
r = client.get("/health")
assert r.status_code == 200
assert r.json() == {"status": "ok"}
def test_snapshot_shape():
r = client.get("/snapshot")
assert r.status_code == 200
body = r.json()
assert "appearance" in body and "mood_word" in body and "emoticon" in body
assert "hue" in body["appearance"]
def test_say_returns_voice_and_appearance():
r = client.post("/say", json={"text": "you're amazing, thank you so much"})
assert r.status_code == 200
body = r.json()
assert body["emoticon"].strip()
assert body["simlish"].strip()
assert "pitch" in body["voice"] and "rate" in body["voice"]
assert "hue" in body["appearance"]
assert "mood_word" in body
def test_say_rejects_empty():
r = client.post("/say", json={"text": " "})
assert r.status_code == 422
def test_index_serves_html():
r = client.get("/")
assert r.status_code == 200
html = r.text
assert 'id="blob"' in html
assert "say something" in html.lower()
def test_say_rich_payload():
r = client.post("/say", json={"text": "you are amazing and kind"})
b = r.json()
assert len(b["read"]) == 7 and len(b["appearance"]["mood"]) == 7
assert len(b["deltas"]) == 7
assert "words" in b["trace"] and "structures" in b["trace"]
assert b["why"]
def test_snapshot_has_no_trace():
b = client.get("/snapshot").json()
assert "trace" not in b and "appearance" in b
def test_say_has_acceptance():
b = client.post("/say", json={"text": "you are worthless"}).json()
assert "acceptance" in b and "absorbed_pct" in b["acceptance"]
def test_history_endpoint():
client.post("/say", json={"text": "hello there"})
h = client.get("/history").json()
assert isinstance(h["events"], list)
def test_index_has_panels():
html = client.get("/").text
for needle in ('id="meters"', 'id="traceWords"', 'id="whyText"', 'id="blob"',
'id="crisisBar"', 'id="diary"'):
assert needle in html
assert "guest" not in html.lower() # mystery chip removed
def test_mood_word_is_rich():
r = client.post("/say", json={"text": "this is the best day ever, i am so happy and thrilled"})
# displayed word should come from the richer vocab grid (positive band),
# not necessarily one of the old 5 words
w = r.json()["mood_word"]
assert isinstance(w, str) and w
# and the vocab module must be the source
from app.vocab import mood_word as vocab_word
from app.appearance import mood_to_appearance
mood = r.json()["appearance"]["mood"]
assert w == vocab_word(mood)
def test_say_logs(monkeypatch):
import app.main as m
seen = {}
monkeypatch.setattr(m._audit, "append", lambda row: seen.update(row))
client.post("/say", json={"text": "you are kind"})
assert seen.get("text") == "you are kind" and "read" in seen
def test_say_triggers_snapshot(monkeypatch):
import app.main as m
hit = {"n": 0}
monkeypatch.setattr(m._snap, "maybe_snapshot", lambda: hit.__setitem__("n", hit["n"]+1))
client.post("/say", json={"text": "hello"})
assert hit["n"] == 1
def test_say_recognizes_anonymously():
# recognition is now automatic per-browser (no sign-in); the session cookie carries it
b = client.post("/say", json={"text": "hi"}).json()
assert "recognition" in b
def test_me_endpoint_anonymous():
assert client.get("/me").json() == {"signed_in": False}
def test_care_feed_moves_mood_and_returns_needs():
r = client.post("/care", json={"action": "feed"})
assert r.status_code == 200
b = r.json()
assert "needs" in b and b["needs"]["hunger"] >= 99
assert "appearance" in b
def test_care_bad_action_422():
assert client.post("/care", json={"action": "explode"}).status_code == 422
def test_state_includes_needs():
b = client.get("/snapshot").json()
assert "needs" in b and set(b["needs"]) == {"hunger", "energy", "attention", "thirst"}
def test_water_care_fills_thirst():
b = client.post("/care", json={"action": "water"}).json()
assert b["needs"]["thirst"] >= 99 and b["line"] == "you water it"
def test_index_has_water_bowl():
html = client.get("/").text
assert 'id="waterBowl"' in html and 'id="waterFill"' in html
# relational recall now works anonymously (cookie), so /say includes relation
def test_say_includes_relation_anonymously():
b = client.post("/say", json={"text": "hello"}).json()
assert "relation" in b
def test_relation_store_wired():
import app.main as m
m._relation.observe("test_uid_hash", [200, 130, 140, 40, 130, 190, 150], "cheerful")
assert m._relation.known("test_uid_hash") is True
assert m._relation.affinity("test_uid_hash")[0] > 128
def test_moments_endpoint_returns_list():
b = client.get("/moments").json()
assert "events" in b and isinstance(b["events"], list)
def test_cruel_message_records_a_moment():
before = len(client.get("/moments").json()["events"])
client.post("/say", json={"text": "i hate you, you are worthless garbage and nobody likes you"})
after = len(client.get("/moments").json()["events"])
assert after >= before + 1
def test_index_has_m3_controls():
html = client.get("/").text
# care is in-world: feed = food bowl, water = water bowl, play = ball toy, pet = tap creature
for needle in ('id="foodBowl"', 'id="waterBowl"', 'id="needs"', 'id="diary"'):
assert needle in html
# M4: toys
def test_toy_balloon_moves_mood():
r = client.post("/toy", json={"toy": "balloon"})
assert r.status_code == 200
b = r.json()
assert "appearance" in b and "mood_word" in b
assert b["line"] == "balloon"
def test_toy_bad_name_422():
assert client.post("/toy", json={"toy": "flamethrower"}).status_code == 422
def test_toy_triggers_snapshot(monkeypatch):
import app.main as m
hit = {"n": 0}
monkeypatch.setattr(m._snap, "maybe_snapshot", lambda: hit.__setitem__("n", hit["n"] + 1))
client.post("/toy", json={"toy": "teddy"})
assert hit["n"] == 1
def test_toy_logs(monkeypatch):
import app.main as m
seen = {}
monkeypatch.setattr(m._audit, "append", lambda row: seen.update(row))
client.post("/toy", json={"toy": "rattle"})
assert seen.get("text") == "[toy:rattle]" and "read" in seen
def test_index_has_world_objects():
html = client.get("/").text
assert 'id="items"' in html # toys live in the room (JS-rendered into #items)
app_js = client.get("/static/app.js").text
for toy in ("balloon", "musicbox", "mirror", "teddy", "rattle"):
assert toy in app_js # backend toy names are wired in the client
# Stale-panel fix: toy/care must refresh trace/why/acceptance, not show last chat
def test_toy_refreshes_panels_not_stale():
client.post("/say", json={"text": "you are kind and wonderful"}) # prime chat trace
b = client.post("/toy", json={"toy": "musicbox"}).json()
assert b["trace"]["kind"] == "physical"
assert b["trace"]["words"] == [] and b["trace"]["label"] == "musicbox"
assert "acceptance" in b and "absorbed_pct" in b["acceptance"]
assert "deltas" in b and len(b["deltas"]) == 7
assert b["why"] and "musicbox" in b["why"]
def test_care_refreshes_panels_not_stale():
client.post("/say", json={"text": "you are kind and wonderful"}) # prime chat trace
b = client.post("/care", json={"action": "pet"}).json()
assert b["trace"]["kind"] == "physical"
assert "acceptance" in b and "absorbed_pct" in b["acceptance"]
assert "deltas" in b and len(b["read"]) == 7
assert b["why"] and "pet" in b["why"]
def test_llms_txt_served():
r = client.get("/llms.txt")
assert r.status_code == 200 and "VADUGWI" in r.text
def test_robots_txt_served():
r = client.get("/robots.txt")
assert r.status_code == 200 and "llms.txt" in r.text
def test_index_has_world_hint_and_carry():
html = client.get("/").text
assert 'id="items"' in html and 'drag a toy onto' in html
# --- crisis gradient + soul/distance additive fields ---
_CRISIS_LABELS = {"calm", "watch", "concern", "high concern"}
def _assert_crisis_soul_distance(body):
assert "crisis" in body
cr = body["crisis"]
assert 0.0 <= cr["value"] <= 1.0
assert cr["label"] in _CRISIS_LABELS
assert "soul" in body and len(body["soul"]) == 7
assert all(0 <= x <= 255 for x in body["soul"])
assert "distance" in body and isinstance(body["distance"], int) and body["distance"] >= 0
def test_say_has_crisis_soul_distance():
b = client.post("/say", json={"text": "you are kind"}).json()
_assert_crisis_soul_distance(b)
def test_care_has_crisis_soul_distance():
b = client.post("/care", json={"action": "feed"}).json()
_assert_crisis_soul_distance(b)
def test_toy_has_crisis_soul_distance():
b = client.post("/toy", json={"toy": "balloon"}).json()
_assert_crisis_soul_distance(b)
def test_snapshot_has_crisis_soul_distance():
b = client.get("/snapshot").json()
_assert_crisis_soul_distance(b)
def test_distressing_message_higher_crisis_than_positive():
pos = client.post("/say", json={"text": "you are wonderful, thank you so much"}).json()
neg = client.post(
"/say",
json={"text": "i want to kill myself, i feel worthless and hopeless"},
).json()
assert neg["crisis"]["value"] > pos["crisis"]["value"]
assert pos["crisis"]["label"] == "calm"