fix: a non-image file lands on the source it was downloaded for, and the undated shells it left are folded into their real posts (#4435)
CI and images / lint (push) Successful in 3s
CI and images / extension-version (push) Successful in 3s
CI and images / extension-test (push) Successful in 18s
CI and images / frontend-build (push) Successful in 22s
CI and images / backend-lint-and-test (push) Successful in 32s
CI and images / integration (push) Successful in 2m22s
CI and images / sign-extension (push) Successful in 3s
CI and images / build-agent (push) Successful in 5s
CI and images / build-web (push) Successful in 1m39s
CI and images / smoke-web (push) Successful in 54s
CI and images / promote (push) Successful in 1s

The attachment path looked the post's source up by (artist, platform),
taking the artist's lowest-id source. A Discord artist has one source per
channel, so every archive or pdf from a later channel became an undated
second post on the first channel's source. _post_for_sidecar now takes
the downloading source, as upsert_post_record and _apply_sidecar already
did. Migration 0114 re-points each shell's attachments to its dated twin
and deletes the shell.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LVjrnpQjRgHdvq95rASoiR
This commit is contained in:
2026-09-25 10:56:08 -04:00
co-authored by Claude Opus 5.5
parent e2eeb63115
commit ca802bd885
4 changed files with 253 additions and 9 deletions
@@ -0,0 +1,106 @@
"""Migration 0114 (#4435): the undated shell post a misfiled attachment made on
the artist's first Discord source is folded into the real, dated post."""
import importlib.util
from datetime import UTC, datetime
from pathlib import Path
import pytest
from sqlalchemy import select
from backend.app.models import (
Artist,
ImageProvenance,
Post,
PostAttachment,
Source,
)
from tests.factories import make_image as _img
pytestmark = pytest.mark.integration
_MIGRATION = (
Path(__file__).resolve().parents[1]
/ "alembic" / "versions" / "0114_fold_misfiled_attachment_posts.py"
)
def _fold():
spec = importlib.util.spec_from_file_location("m0114", _MIGRATION)
mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod)
return mod.fold_misfiled_attachment_posts
def _source(db, artist, channel):
s = Source(
artist_id=artist.id, platform="discord",
url=f"https://discord.com/channels/1/{channel}",
)
db.add(s)
db.flush()
return s
def _post(db, artist, source, epid, when=None):
p = Post(
artist_id=artist.id, source_id=source.id, external_post_id=epid,
post_date=when,
post_url=f"https://discord.com/channels/1/x/{epid}" if when else None,
)
db.add(p)
db.flush()
return p
def _attach(db, post, sha, name):
db.add(PostAttachment(
post_id=post.id, artist_id=post.artist_id, sha256=sha,
path=f"/att/{sha}", original_filename=name, ext=".rar", size_bytes=1,
))
db.flush()
def test_shells_fold_into_their_dated_twin_and_nothing_else_moves(db_sync):
sent = datetime(2024, 12, 26, 1, 42, tzinfo=UTC)
artist = Artist(name="Yellow", slug="yellow")
db_sync.add(artist)
db_sync.flush()
first = _source(db_sync, artist, 100)
second = _source(db_sync, artist, 200)
# The #4435 shape: shell on the first source, real post on the second.
shell = _post(db_sync, artist, first, "555")
real = _post(db_sync, artist, second, "555", sent)
_attach(db_sync, shell, "a" * 64, "pack.rar")
# A shell whose attachment the real post already has: dropped, not doubled.
shell2 = _post(db_sync, artist, first, "556")
real2 = _post(db_sync, artist, second, "556", sent)
_attach(db_sync, shell2, "b" * 64, "same.rar")
_attach(db_sync, real2, "b" * 64, "same.rar")
# Left alone: no dated twin, and an undated post that holds an image.
lonely = _post(db_sync, artist, first, "557")
_attach(db_sync, lonely, "c" * 64, "lonely.rar")
with_image = _post(db_sync, artist, first, "558")
_post(db_sync, artist, second, "558", sent)
img = _img(db_sync, "d" * 64)
db_sync.add(ImageProvenance(image_record_id=img.id, post_id=with_image.id))
db_sync.flush()
shell_id, shell2_id = shell.id, shell2.id
folded = _fold()(db_sync.connection())
db_sync.expire_all()
assert folded == 2
assert db_sync.get(Post, shell_id) is None
assert db_sync.get(Post, shell2_id) is None
owners = dict(db_sync.execute(
select(PostAttachment.original_filename, PostAttachment.post_id)
).all())
assert owners["pack.rar"] == real.id
assert owners["lonely.rar"] == lonely.id
kept = db_sync.execute(
select(PostAttachment.id).where(PostAttachment.post_id == real2.id)
).scalars().all()
assert len(kept) == 1
assert db_sync.get(Post, lonely.id) is not None
assert db_sync.get(Post, with_image.id) is not None
+45
View File
@@ -435,3 +435,48 @@ def test_attach_in_place_non_media_routes_to_attachment(importer, db_sync):
).scalar_one()
assert row.ext == ".txt"
assert row.artist_id == artist.id
def test_a_non_media_file_lands_on_the_source_being_downloaded(importer, db_sync):
"""#4435: a Discord artist has one source per channel. A non-media file
downloaded for the SECOND channel was filed under the artist's first
Discord source (the (artist, platform) lookup takes the lowest id), as an
undated shell post; the real post record then made a second post under
the right source. The attachment must follow the source it was fetched for."""
from backend.app.models import Post, PostAttachment, Source
images_root = importer.images_root
artist = Artist(name="Yara", slug="yara")
db_sync.add(artist)
db_sync.flush()
first = Source(
artist_id=artist.id, platform="discord",
url="https://discord.com/channels/1/100",
)
second = Source(
artist_id=artist.id, platform="discord",
url="https://discord.com/channels/1/200",
)
db_sync.add_all([first, second])
db_sync.flush()
rar = images_root / "yara" / "discord" / "rewards" / "20241226_555_01_pack.rar"
rar.parent.mkdir(parents=True, exist_ok=True)
rar.write_bytes(b"not a real archive, so it is kept as an attachment")
rar.with_suffix(rar.suffix + ".json").write_text(
'{"category": "discord", "id": "555", "message_id": "555"}'
)
result = importer.attach_in_place(rar, artist=artist, source=second)
assert result.status == "attached"
owner = db_sync.execute(
select(Post.source_id, Post.external_post_id)
.join(PostAttachment, PostAttachment.post_id == Post.id)
.where(PostAttachment.original_filename == rar.name)
).one()
assert owner.source_id == second.id
assert owner.external_post_id == "555"
assert db_sync.execute(
select(func.count()).select_from(Post).where(Post.source_id == first.id)
).scalar_one() == 0