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>
This commit is contained in:
@@ -0,0 +1,134 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user