Files
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

135 lines
6.2 KiB
Python

"""Memory Match server state — light, durability-only (no anti-cheat; the board is
deterministic and fully visible). Locks: malformed keys dropped, matched stored as
deduped face KEYS, `done` DERIVED from the matched count vs the tier's target (never
trusted from the client), cross-device merge unions matched, and the sync endpoint
accepts only valid match variants."""
import pytest
from fastapi.testclient import TestClient
from goodnews import games
from goodnews.db import connect, init_db
def _san(variant, state):
return games.sanitize_game_state(None, "match", variant, "2026-06-16", state)
def test_sanitize_drops_junk_and_dedupes():
s = _san("standard-icons", {
"matched": ["leaf", "leaf", "color-rose", "banana", "BAD KEY!", 42, "x" * 40, "sun"],
"moves": -5, "done": "yes",
})
# deduped + validated against the real face set ("banana"/junk dropped), order kept
assert s["matched"] == ["leaf", "color-rose", "sun"]
assert s["moves"] == 0 # clamped ≥ 0
assert s["done"] is False # 3 < 8 faces — client's "yes" ignored
def test_done_is_derived_from_matched_count_not_client_flag():
real8 = ["leaf", "sun", "star", "moon", "cloud", "wave", "tree", "heart"] # real faces
# client lies that it's done with no progress → server says not done
assert _san("standard-icons", {"matched": [], "done": True})["done"] is False
# reaching the tier's face target (standard = 8) → done
assert _san("standard-icons", {"matched": real8, "done": False})["done"] is True
# gentle target is only 6
assert _san("gentle-icons", {"matched": real8[:6]})["done"] is True
assert _san("standard-icons", {"matched": real8[:6]})["done"] is False
def test_sanitize_caps_face_count():
many = ["color-rose", "color-coral", "color-amber", "color-gold", "color-lime",
"color-green", "color-teal", "color-cyan", "color-sky", "color-blue",
"color-indigo", "color-violet", "color-plum", "color-brown", "color-sand"] # 15 real
s = _san("expert-colors", {"matched": many})
assert len(s["matched"]) == 12 # _MATCH_MAX_FACES
def test_merge_unions_matched_and_keeps_moves_without_trusting_done():
a = {"matched": ["leaf", "sun"], "moves": 7, "done": False}
b = {"matched": ["sun", "star"], "moves": 4, "done": True}
m = games.merge_game_state("match", a, b)
assert sorted(m["matched"]) == ["leaf", "star", "sun"] # union
assert m["moves"] == 7 # larger move count
assert "done" not in m # merge doesn't carry done; sanitize derives it
@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 _signin(app, email="p@b.com"):
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": email})
tc.post("/api/auth/email/verify", json={"token": sent["link"].split("token=")[1]})
finally:
es.send_magic_link = orig
return tc
def _put(tc, variant, state):
return tc.put("/api/games/state", json={
"game": "match", "variant": variant, "date": "2026-06-16", "state": state})
def test_sync_endpoint_flow(api_app):
tc = _signin(api_app)
r1 = _put(tc, "standard-icons", {"matched": ["leaf", "sun"], "moves": 3, "done": False})
assert r1.status_code == 200
assert sorted(r1.json()["state"]["matched"]) == ["leaf", "sun"]
assert r1.json()["state"]["done"] is False
# a second device merges in (still partial → not done)
r2 = _put(tc, "standard-icons", {"matched": ["star"], "moves": 1, "done": True})
assert sorted(r2.json()["state"]["matched"]) == ["leaf", "star", "sun"]
assert r2.json()["state"]["done"] is False # 3 < 8, client's done ignored
# completing the board (8 real faces) → done
r3 = _put(tc, "standard-icons",
{"matched": ["leaf", "sun", "star", "moon", "cloud", "wave", "tree", "heart"], "moves": 12})
assert r3.json()["state"]["done"] is True
# unknown variant rejected
assert _put(tc, "huge-icons", {}).status_code == 404
def test_batch_endpoint_reconciles_many_and_drops_bad(api_app):
tc = _signin(api_app)
body = {"date": "2026-06-16", "items": [
{"game": "match", "variant": "standard-icons", "state": {"matched": ["leaf", "sun"], "moves": 2}},
{"game": "bloom", "variant": "", "state": {"found": []}},
{"game": "match", "variant": "bogus-xyz", "state": {}}, # unknown variant → dropped
]}
r = tc.put("/api/games/state/batch", json=body)
assert r.status_code == 200
states = r.json()["states"]
variants = {(s["game"], s["variant"]) for s in states}
assert ("match", "standard-icons") in variants
assert ("bloom", "") in variants
assert ("match", "bogus-xyz") not in variants # invalid item dropped, not fatal
m = next(s for s in states if s["variant"] == "standard-icons")
assert sorted(m["state"]["matched"]) == ["leaf", "sun"] # merged + sanitized
# a second device merges via the same batch path
r2 = tc.put("/api/games/state/batch", json={"date": "2026-06-16", "items": [
{"game": "match", "variant": "standard-icons", "state": {"matched": ["star"], "moves": 5}}]})
m2 = r2.json()["states"][0]["state"]
assert sorted(m2["matched"]) == ["leaf", "star", "sun"] and m2["moves"] == 5
def test_batch_endpoint_signed_out_echoes(api_app):
from fastapi.testclient import TestClient
r = TestClient(api_app).put("/api/games/state/batch", json={"date": "2026-06-16", "items": [
{"game": "match", "variant": "gentle-colors", "state": {"matched": ["color-rose"]}}]})
assert r.status_code == 200
assert r.json()["states"][0]["state"] == {"matched": ["color-rose"]} # echo, no sync