"""#4402 / #4401: a teaser's card shows what it points at — by reference. Operator, 2026-09-24: *"the teaser from the patreon post doesn't show the items that it's supposed to reference"*, and on how: *"discord 'posts' land as normal and only hidden from the post view they're posted the same day. the nested items on the unified post are a duplicate or reference of existing content."* Two properties carry the whole design, and most of what follows pins them: * NOTHING MOVES. A referenced Discord post keeps its row, its date and its place in the feed; only a drop sitting beside its own teaser is left out. * A family is the creator's LEADING working name, one hop from the seed, inside a window. The refusals are the measured false positives, not invented ones. """ from collections import Counter from datetime import UTC, datetime, timedelta import pytest from sqlalchemy import select from backend.app.models import ( Artist, ImageRecord, ImportSettings, Post, PostAssociation, Source, ) from backend.app.services.discord_grouping import DROP_GROUPER from backend.app.services.post_association_service import PostAssociationService from backend.app.services.post_feed_service import PostFeedService from backend.app.services.post_unification import ( FAMILY_MAX_POSTS, Candidate, family, ) T0 = datetime(2026, 9, 1, 12, 0, tzinfo=UTC) WINDOW = timedelta(days=60) def _c(image_id, name, *, days=0, post_id=None, phash=None): """One image, Discord-shaped, `days` from T0.""" return Candidate( image_id=image_id, post_id=post_id if post_id is not None else image_id, path=f"20260901_12345678901234{image_id:04d}_01_{name}.png", phash=phash, at=T0 + timedelta(days=days), ) def _family(seed, pool, *, names=None, hashes=None): return family( seed, pool, Counter(names or {}), Counter(hashes or {}), anchor=T0, window=WINDOW, ) # --- the family rule, pure --------------------------------------------------- def test_the_older_wips_of_the_same_piece_are_its_family(): """The operator's ask exactly: *"yellowroom trickles out variants and I want them to show in the grouped post even if they're older"*. Measured shape: `Year_20k_wip1 -> wip3 -> Base -> Cndm` over 44 days.""" seed = [_c(1, "Year_20k_Cndm")] pool = [_c(2, "Year_20k_wip1", days=-44), _c(3, "Year_20K_wip3", days=-44), _c(4, "Year_20k_Base", days=-20)] assert [c.image_id for c in _family(seed, pool)] == [2, 3, 4] def test_a_family_reads_oldest_first(): """So the card shows the trickle in the order it happened.""" seed = [_c(1, "svtt_drench_b")] pool = [_c(2, "svtt_wip5", days=-1), _c(3, "svtt_wip1", days=-3)] assert [c.image_id for c in _family(seed, pool)] == [3, 2] def test_a_namesake_outside_the_window_is_not_family(): """Time does most of the work. Measured on artist 8: every collision found spreads over 500 days — `ashley` 1258, `anya` 1217, `image0` 1974.""" seed = [_c(1, "Ashley_TAIGA")] pool = [_c(2, "Ashley_Re4_A", days=-1258)] assert _family(seed, pool) == [] def test_a_shared_trailing_word_is_not_family(): """The one plain collision inside 60 days on artist 8: `bottom`, three posts, 56 days apart. No frequency cap can refuse a word that rare — the leading-name rule does.""" seed = [_c(1, "Undyne_insert_bottom_only-C")] pool = [_c(2, "Lichgalclc_Lingerie_Bottom_21", days=-56)] assert _family(seed, pool) == [] def test_a_leading_name_the_creator_uses_everywhere_is_not_a_family(): """A character is a habit, not a piece. At the cap the name is gated even inside the window.""" seed = [_c(1, "Bea_Machamp_Shiny")] pool = [_c(2, "Bea_Machoke_Shiny", days=-5)] assert _family(seed, pool, names={"bea": FAMILY_MAX_POSTS}) == [] def test_the_family_cap_admits_the_measured_long_families(): """`tentacooler` spans 6 posts over 7 days and `0-k1` 6 over 10 — real families, both lost at the pairing cap of 6. That is why the family cap is its own number.""" seed = [_c(1, "Tentacooler")] pool = [_c(2, "Tentacooler_c_ins", days=-7)] assert [c.image_id for c in _family(seed, pool, names={"tentacooler": 6})] == [2] def test_a_near_duplicate_joins_the_family_without_a_name(): """Half of this creator's teasers are screenshots, which carry no name. The same file re-posted is still the same file.""" seed = [_c(1, "image0", phash=0b1011)] pool = [_c(2, "image0", days=-10, phash=0b1010)] assert [c.image_id for c in _family(seed, pool)] == [2] def test_a_distant_hash_is_not_a_duplicate(): """Lesson #4400: same-artist images are similar whether or not they are the same piece, so only a NEAR-duplicate counts — the matcher's own line.""" seed = [_c(1, "image0", phash=0)] pool = [_c(2, "image0", days=-10, phash=(1 << 100) - 1)] assert _family(seed, pool) == [] def test_a_family_is_one_hop_from_the_seed(): """Chaining is what lets a family drift: `alpha` reaches an image that also carries `beta`, and `beta` must not then reach its own family.""" seed = [_c(1, "alpha_final")] pool = [_c(2, "alpha_beta", days=-3), _c(3, "beta_wip1", days=-4)] assert [c.image_id for c in _family(seed, pool)] == [2] def test_the_seed_never_comes_back_as_its_own_family(): seed = [_c(1, "Year_20k_Cndm")] assert _family(seed, seed + [_c(2, "Year_20k_Base", days=-20)])[0].image_id == 2 assert len(_family(seed, seed)) == 0 # --- the card, end to end ---------------------------------------------------- async def _channels(db, name): artist = Artist(name=name, slug=name) db.add(artist) await db.flush() patreon = Source(artist_id=artist.id, platform="patreon", url=f"https://patreon.com/{name}", enabled=True) discord = Source(artist_id=artist.id, platform="discord", url=f"https://discord.com/channels/1/{name}", enabled=True) db.add_all([patreon, discord]) await db.flush() return artist, patreon, discord _seq = iter(range(1, 10_000)) async def _post(db, artist, source, *, at, names, body=None, title=None, synthetic=False): post = Post( source_id=source.id, artist_id=artist.id, external_post_id=f"ext-{next(_seq)}", post_date=at, post_title=title, description=body, synthesized_by=DROP_GROUPER if synthetic else None, ) db.add(post) await db.flush() for name in names: n = next(_seq) db.add(ImageRecord( # Discord-shaped whatever the platform: the prefix is stripped, so # the NAME below is the leading name. path=f"/images/{artist.slug}/20260901_1234567890{n:06d}_01_{name}.png", sha256=f"{n:064d}", size_bytes=10, mime="image/png", width=10, height=10, origin="downloaded", primary_post_id=post.id, artist_id=artist.id, )) await db.flush() return post async def _link(db, teaser, drop, *, status="linked", linked_by="fc"): a = PostAssociation( announcement_post_id=teaser.id, payload_post_id=drop.id, score=1.0, signals={"identity": 1.0, "identity_token": "year"}, status=status, linked_by=linked_by if status == "linked" else None, ) db.add(a) await db.flush() return a async def _feed(db, artist): page = await PostFeedService(db).scroll(artist_id=artist.id, limit=100) return {item["id"]: item for item in page["items"]} @pytest.mark.integration @pytest.mark.asyncio async def test_a_teaser_shows_the_drop_it_announced(db): """The complaint itself: the card now carries the drop's images and text.""" artist, patreon, discord = await _channels(db, "unifyartist") teaser = await _post(db, artist, patreon, at=T0, names=["Year_20k_teaser"], title="Year 20k", body="Full set in the server") drop = await _post(db, artist, discord, at=T0 - timedelta(hours=1), names=["Year_20k_Cndm", "Year_20k_Cndm_alt"], body="@everyone 🍈🍈 the full set", synthetic=True) assoc = await _link(db, teaser, drop) await db.commit() unified = (await _feed(db, artist))[teaser.id]["unified"] assert [t["role"] for t in unified["thumbnails"]] == ["drop", "drop"] assert {t["post_id"] for t in unified["thumbnails"]} == {drop.id} assert unified["links"] == [{ "association_id": assoc.id, "post_id": drop.id, "linked_by": "fc", "token": "year", }] assert unified["texts"][0]["text"] == "@everyone 🍈🍈 the full set" @pytest.mark.integration @pytest.mark.asyncio async def test_a_drop_beside_its_teaser_leaves_the_feed(db): """*"only hidden from the post view they're posted the same day"*.""" artist, patreon, discord = await _channels(db, "foldartist") teaser = await _post(db, artist, patreon, at=T0, names=["Year_20k_teaser"]) drop = await _post(db, artist, discord, at=T0 - timedelta(hours=2), names=["Year_20k_Cndm"], synthetic=True) await _link(db, teaser, drop) await db.commit() feed = await _feed(db, artist) assert teaser.id in feed assert drop.id not in feed # Left out of the FEED, not out of existence: reachable by id, as every # post is, because it is still the images' true origin. assert (await PostFeedService(db).get_post(drop.id))["id"] == drop.id @pytest.mark.integration @pytest.mark.asyncio async def test_a_drop_days_from_its_teaser_keeps_its_place(db): """A reference does not take anything out of history.""" artist, patreon, discord = await _channels(db, "keepartist") teaser = await _post(db, artist, patreon, at=T0, names=["Year_20k_teaser"]) drop = await _post(db, artist, discord, at=T0 - timedelta(days=3), names=["Year_20k_Cndm"], synthetic=True) await _link(db, teaser, drop) await db.commit() feed = await _feed(db, artist) assert drop.id in feed assert feed[teaser.id]["unified"]["thumbnails"][0]["post_id"] == drop.id @pytest.mark.integration @pytest.mark.asyncio async def test_a_proposal_changes_nothing_on_the_card(db): """Only a LINKED pair unifies. A pending proposal is a question for the review queue, and rendering it would assert a link nobody made.""" artist, patreon, discord = await _channels(db, "pendingartist") teaser = await _post(db, artist, patreon, at=T0, names=["Year_20k_teaser"]) drop = await _post(db, artist, discord, at=T0 - timedelta(hours=1), names=["Year_20k_Cndm"], synthetic=True) await _link(db, teaser, drop, status="pending") await db.commit() feed = await _feed(db, artist) assert feed[teaser.id]["unified"] is None assert drop.id in feed @pytest.mark.integration @pytest.mark.asyncio async def test_the_older_variants_come_along_and_nothing_else(db): """The family reaches back 44 days for the wips — and not 90 days for a namesake, and not at all for a message that only shares a trailing word.""" artist, patreon, discord = await _channels(db, "familyartist") teaser = await _post(db, artist, patreon, at=T0, names=["Year_20k_teaser"]) drop = await _post(db, artist, discord, at=T0 - timedelta(hours=1), names=["Year_20k_Cndm"], synthetic=True) wip = await _post(db, artist, discord, at=T0 - timedelta(days=44), names=["Year_20k_wip1"], body="wip, feedback welcome") base = await _post(db, artist, discord, at=T0 - timedelta(days=20), names=["Year_20K_Base"]) too_old = await _post(db, artist, discord, at=T0 - timedelta(days=90), names=["Year_20k_old"]) trailing = await _post(db, artist, discord, at=T0 - timedelta(days=5), names=["Other_piece_year"]) await _link(db, teaser, drop) await db.commit() feed = await _feed(db, artist) unified = feed[teaser.id]["unified"] variants = [t["post_id"] for t in unified["thumbnails"] if t["role"] == "variant"] assert variants == [wip.id, base.id] assert unified["variant_count"] == 2 assert too_old.id not in variants and trailing.id not in variants assert "wip, feedback welcome" in [t["text"] for t in unified["texts"]] # Referenced, not moved: every variant is still in the feed on its own. assert {wip.id, base.id} <= set(feed) @pytest.mark.integration @pytest.mark.asyncio async def test_the_family_window_is_the_operators_setting(db): artist, patreon, discord = await _channels(db, "windowartist") teaser = await _post(db, artist, patreon, at=T0, names=["Year_20k_teaser"]) drop = await _post(db, artist, discord, at=T0 - timedelta(hours=1), names=["Year_20k_Cndm"], synthetic=True) await _post(db, artist, discord, at=T0 - timedelta(days=20), names=["Year_20k_wip1"]) await _link(db, teaser, drop) settings = await db.get(ImportSettings, 1) settings.discord_family_window_days = 10 await db.commit() unified = (await _feed(db, artist))[teaser.id]["unified"] assert unified["variant_count"] == 0 @pytest.mark.integration @pytest.mark.asyncio async def test_undo_is_a_dismissal_and_everything_returns(db): """The operator chose *"nest automatically, with visible undo"*. The undo is the review queue's own dismiss: the drop comes back to the feed, the card loses its references, and the dismissed row is what stops the next sweep linking the pair straight back.""" artist, patreon, discord = await _channels(db, "undoartist") teaser = await _post(db, artist, patreon, at=T0, names=["Year_20k_teaser"]) drop = await _post(db, artist, discord, at=T0 - timedelta(hours=1), names=["Year_20k_Cndm"], synthetic=True) assoc = await _link(db, teaser, drop) await db.commit() await PostAssociationService(db).dismiss(assoc.id) await db.commit() feed = await _feed(db, artist) assert feed[teaser.id]["unified"] is None assert drop.id in feed row = (await db.execute(select(PostAssociation))).scalar_one() assert (row.status, row.linked_by) == ("dismissed", None) @pytest.mark.integration @pytest.mark.asyncio async def test_an_operator_accept_is_recorded_as_theirs(db): """So the card does not claim FC made a link a person made.""" artist, patreon, discord = await _channels(db, "acceptartist") teaser = await _post(db, artist, patreon, at=T0, names=["Year_20k_teaser"]) drop = await _post(db, artist, discord, at=T0 - timedelta(hours=1), names=["Year_20k_Cndm"], synthetic=True) assoc = await _link(db, teaser, drop, status="pending") await db.commit() await PostAssociationService(db).accept(assoc.id) await db.commit() unified = (await _feed(db, artist))[teaser.id]["unified"] assert unified["links"][0]["linked_by"] == "operator" @pytest.mark.integration @pytest.mark.asyncio async def test_a_fold_window_of_zero_hides_nothing(db): artist, patreon, discord = await _channels(db, "nofoldartist") teaser = await _post(db, artist, patreon, at=T0, names=["Year_20k_teaser"]) drop = await _post(db, artist, discord, at=T0 - timedelta(hours=1), names=["Year_20k_Cndm"], synthetic=True) await _link(db, teaser, drop) settings = await db.get(ImportSettings, 1) settings.discord_link_fold_hours = 0 await db.commit() assert drop.id in await _feed(db, artist)