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"