Files
upbeatBytes/tests/test_bloom.py
T
thejayman77 89c0fbe1f6 Sync repo to deployed state: SEO recovery, Publishing Desk, Play games, emoji picker
The deploy pipeline runs from the working tree, so a wave of shipped features
had never been committed. This snapshots git to what's actually running.

SEO impression recovery (live + verified):
- Duplicate /a/{id} now 301-redirect to their canonical twin instead of 404
  (a hard 404 silently dropped already-indexed URLs and tanked impressions).
- Dedup representative selection reworked: accepted/serveable -> established
  rep (URL stability) -> quality score, so an accepted page never retires to a
  rejected rep and an indexed canonical doesn't churn when a newer twin arrives.
- HEAD /a/{id} returns the same status as GET (api_route GET+HEAD) instead of
  falling through to the static mount and 404ing.
- `dedup --force-recluster`: cycle-locked, model-free re-cluster to re-apply the
  policy to the existing corpus (shared cycle_lock context manager).
- CLI honors GOODNEWS_DB for its default --db (was silently ignored).

Publishing Desk (admin tool to post highlights to X via Web Intents):
- publishing.py queue/rank/handle-resolution; admin UI; full searchable emoji
  picker (bundled data, no CDN) for the blurb editor.

Play games + site:
- Bloom (word-wheel), Memory Match, daily ritual set, Zen Den (dev-gated).
- English-only language gate; source prospecting; paywall + dedup hardening.

Tests: full suite green (349). Ignores tightened (node_modules, data/*.db).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 11:32:27 -04:00

256 lines
11 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Bloom — the daily word wheel. Locks the design/acceptance split:
• DESIGN (deterministic, stored): wheel + tiers + pangram + Full-Bloom target,
from the COMMON list. The PERMANENT guardrail — Flourishing reachable with
common words — still holds.
• ACCEPTANCE (broad + dynamic): every valid word buildable from the wheel,
computed live as broad dict {allow} {block}; runtime admin overrides +
player reports drive curation with no deploy.
"""
import os
from pathlib import Path
import pytest
from fastapi.testclient import TestClient
from goodnews import bloom, games
from goodnews.db import connect, init_db
DATES = [f"2026-06-{d:02d}" for d in range(10, 25)] # 15 sample days
@pytest.fixture(scope="module")
def designs():
return {d: bloom.build_puzzle(d) for d in DATES}
@pytest.fixture
def conn(tmp_path):
c = connect(str(tmp_path / "t.db"))
init_db(c)
c.execute("INSERT INTO users (email) VALUES ('a@b.c')")
c.commit()
return c
def _letters(p):
return frozenset(p["center"]) | frozenset(p["outer"])
def _commons_for(p):
"""COMMON words for a center-mode wheel (the designed puzzle)."""
L = _letters(p)
return [w for w in bloom._COMMON if p["center"] in w and frozenset(w) <= L]
def _assert_no_answer_leak(resp):
assert "words" not in resp
assert resp["accepted"] and all(
isinstance(h, str) and len(h) == 64 and set(h) <= set("0123456789abcdef")
for h in resp["accepted"])
# --- DESIGN (deterministic, common-based) --------------------------------------
def test_build_is_deterministic():
assert bloom.build_puzzle("2026-06-15") == bloom.build_puzzle("2026-06-15")
@pytest.mark.parametrize("date", DATES)
def test_design_shape(designs, date):
p = designs[date]
L = _letters(p)
assert len(L) == 7 and "s" not in L
assert p["center"] in L and len(p["outer"]) == 6
assert bloom.MIN_COMMON_WORDS <= len(_commons_for(p)) <= bloom.MAX_COMMON_WORDS
assert frozenset(p["pangram"]) == L # display pangram uses all 7
assert p["pangram"] in bloom._COMMON and p["pangram"] not in bloom._AVOID
@pytest.mark.parametrize("date", DATES)
def test_PERMANENT_top_tier_reachable_with_common_words(designs, date):
"""Flourishing reachable from COMMON words alone — never obscure-word hunting."""
p = designs[date]
flourishing = next(t["score"] for t in p["tiers"] if t["name"] == "Flourishing")
assert bloom.score_words(p, _commons_for(p)) >= flourishing
def test_tiers_are_8_30_70_of_common_and_max_is_common_total():
p = bloom.build_puzzle("2026-06-15")
assert [t["name"] for t in p["tiers"]] == ["Sprouting", "Budding", "Blooming", "Flourishing"]
common_total = bloom.score_words(p, _commons_for(p))
assert p["max_score"] == common_total # Full Bloom = the designed puzzle
flour = next(t["score"] for t in p["tiers"] if t["name"] == "Flourishing")
assert flour == int(0.70 * common_total) and flour <= p["max_score"]
# --- ACCEPTANCE (broad + dynamic) ----------------------------------------------
def test_accept_is_broad_and_obeys_center_rule(conn):
p = bloom.build_puzzle("2026-06-15")
acc = bloom.accepted_words(conn, p["center"], p["outer"], require_center=True)
L = _letters(p)
for w in acc:
assert len(w) >= 4 and "s" not in w and frozenset(w) <= L and p["center"] in w
# broad accept is a SUPERSET of the common puzzle (bonus words beyond design)
assert set(_commons_for(p)) <= set(acc)
assert len(acc) > len(_commons_for(p))
def test_arraign_class_words_auto_accepted():
# broad dict includes real-but-rare words without any include-list
for w in ("arraign", "feign", "crwth"):
assert w in set(bloom.ACCEPT)
def test_overrides_block_and_allow(conn):
p = bloom.build_puzzle("2026-06-15")
acc0 = set(bloom.accepted_words(conn, p["center"], p["outer"], True))
victim = sorted(acc0)[0]
bloom.set_override(conn, victim, "block", by="t")
assert victim not in set(bloom.accepted_words(conn, p["center"], p["outer"], True))
# allow a made-up letter-combo that fits the wheel + center
fake = (p["center"] + "".join(p["outer"][:3]))[:5]
if "s" not in fake and len(fake) >= 4:
bloom.set_override(conn, fake, "allow", by="t")
assert fake in set(bloom.accepted_words(conn, p["center"], p["outer"], True))
bloom.clear_override(conn, victim)
assert victim in set(bloom.accepted_words(conn, p["center"], p["outer"], True))
def test_allow_override_rejects_inert_hard_rule_words(conn):
# an allow that could never count (too short / has 's') is rejected, not stored
assert bloom.set_override(conn, "cat", "allow") is False # < 4 letters
assert bloom.set_override(conn, "roses", "allow") is False # contains 's'
assert bloom.set_override(conn, "bloom", "allow") is True # valid → stored
allow, _ = bloom.overrides(conn)
assert allow == {"bloom"}
# block stays permissive (can block anything)
assert bloom.set_override(conn, "roses", "block") is True
def test_wild_accepts_words_without_center(conn):
p = bloom.build_free("seed-w", "wild")
acc = bloom.accepted_words(conn, p["center"], p["outer"], require_center=False)
assert any(p["center"] not in w for w in acc) # Wild's defining trait
assert all(frozenset(w) <= _letters(p) for w in acc)
# --- responses + storage -------------------------------------------------------
def test_generate_is_idempotent_and_stored(conn):
a = bloom.generate_bloom_puzzle(conn, "2026-06-15")
assert a == bloom.generate_bloom_puzzle(conn, "2026-06-15") == bloom.stored_payload(conn, "2026-06-15")
assert "words" not in a # design payload holds no answers
def test_response_no_leak_and_hash_roundtrip(conn):
r = bloom.bloom_response(conn, "2026-06-15")
_assert_no_answer_leak(r)
p = bloom.stored_payload(conn, "2026-06-15")
real = bloom.accepted_words(conn, p["center"], p["outer"], True)[0]
assert bloom.word_hash("2026-06-15", real) in set(r["accepted"])
assert bloom.word_hash("2026-06-15", "zzzzq") not in set(r["accepted"])
assert r["max_score"] == p["max_score"]
def test_free_endpoint_resumes_and_leaks_nothing(api_app):
tc = TestClient(api_app)
r1 = tc.get("/api/puzzle/bloom/free?format=wild").json()
seed = r1["seed"]
assert r1["mode"] == "free" and r1["format"] == "wild" and seed
r2 = tc.get(f"/api/puzzle/bloom/free?format=wild&seed={seed}").json()
assert r2["center"] == r1["center"] and r2["outer"] == r1["outer"]
_assert_no_answer_leak(r1)
# --- server-side state ---------------------------------------------------------
def test_sanitize_drops_junk_recomputes_score_and_full(conn):
p = bloom.generate_bloom_puzzle(conn, "2026-06-15")
acc = bloom.accepted_words(conn, p["center"], p["outer"], True)
good = acc[:3]
clean = games.sanitize_game_state(conn, "bloom", "", "2026-06-15",
{"found": good + ["zzzz", "ab", good[0], 9], "score": 9999})
assert sorted(clean["found"]) == sorted(set(good))
assert clean["score"] == bloom.score_words(p, good)
assert "full" not in clean
# finding the whole common puzzle ⇒ Full Bloom (score ≥ max_score)
full = games.sanitize_game_state(conn, "bloom", "", "2026-06-15", {"found": _commons_for(p)})
assert full.get("full") is True
def test_merge_unions_found():
m = games.merge_game_state("bloom", {"found": ["able", "bake"]}, {"found": ["bake", "tale"]})
assert sorted(m["found"]) == ["able", "bake", "tale"]
def test_block_override_takes_effect_without_regen(conn):
# the live response reflects an override with no puzzle regeneration
p = bloom.generate_bloom_puzzle(conn, "2026-06-15")
victim = bloom.accepted_words(conn, p["center"], p["outer"], True)[0]
before = set(bloom.bloom_response(conn, "2026-06-15")["accepted"])
bloom.set_override(conn, victim, "block", by="t")
after = set(bloom.bloom_response(conn, "2026-06-15")["accepted"])
assert bloom.word_hash("2026-06-15", victim) in before
assert bloom.word_hash("2026-06-15", victim) not in after
# --- reports → admin queue → overrides -----------------------------------------
def test_report_then_approve_creates_allow_override(conn):
assert bloom.add_report(conn, "arraign", "2026-06-15", "daily", "center", "aceglnr", "not in the word list")
assert bloom.add_report(conn, "arraign", "2026-06-15", "daily", "center", "aceglnr", "x") # dedup pending
pending = bloom.list_reports(conn, "pending")
assert len(pending) == 1 and pending[0]["word"] == "arraign"
assert bloom.resolve_report(conn, pending[0]["id"], "approve", by="admin")
allow, _ = bloom.overrides(conn)
assert "arraign" in allow
assert not bloom.list_reports(conn, "pending")
assert bloom.list_reports(conn, "approved")
def test_report_block_creates_block_override(conn):
bloom.add_report(conn, "uglyword", None, "free", "wild", "abcdefg", "x")
rid = bloom.list_reports(conn, "pending")[0]["id"]
bloom.resolve_report(conn, rid, "block", by="admin")
_, block = bloom.overrides(conn)
assert "uglyword" in block
# --- API: public report + admin endpoints --------------------------------------
@pytest.fixture
def api_app(tmp_path, monkeypatch):
db = tmp_path / "t.sqlite3"
monkeypatch.setenv("GOODNEWS_DB", str(db))
monkeypatch.setenv("GOODNEWS_PUBLIC_BASE_URL", "http://testserver")
monkeypatch.setenv("GOODNEWS_ADMIN_EMAILS", "admin@b.com")
import importlib
import goodnews.api as api
importlib.reload(api)
c = connect(str(db)); init_db(c); c.commit(); c.close()
return api.create_app()
def _admin(app):
tc = TestClient(app)
sent = {}
import goodnews.email_send as es
orig = es.send_magic_link
es.send_magic_link = lambda to, link: sent.update(link=link)
try:
tc.post("/api/auth/email/start", json={"email": "admin@b.com"})
tc.post("/api/auth/email/verify", json={"token": sent["link"].split("token=")[1]})
finally:
es.send_magic_link = orig
return tc
def test_public_report_then_admin_queue_flow(api_app):
pub = TestClient(api_app)
assert pub.post("/api/bloom/report", json={"word": "arraign", "date": "2026-06-15",
"mode": "daily", "format": "center", "letters": "aceglnr",
"reason": "not in the word list"}).json()["ok"]
# admin-only queue
assert TestClient(api_app).get("/api/admin/bloom/reports").status_code == 401
tc = _admin(api_app)
q = tc.get("/api/admin/bloom/reports").json()
assert len(q["reports"]) == 1
rid = q["reports"][0]["id"]
assert tc.post(f"/api/admin/bloom/reports/{rid}", json={"action": "approve"}).json()["ok"]
ovr = tc.get("/api/admin/bloom/reports").json()["overrides"]
assert any(o["word"] == "arraign" and o["action"] == "allow" for o in ovr)