CI / lint (push) Successful in 3s
CI / extension-version (push) Successful in 4s
Build images / sign-extension (push) Successful in 4s
Build images / build-agent (push) Successful in 7s
CI / frontend-build (push) Successful in 25s
extension / lint (push) Successful in 26s
CI / backend-lint-and-test (push) Successful in 34s
Build images / build-web (push) Successful in 1m5s
Build images / smoke-web (push) Skipped
Build images / build-ml (push) Successful in 1m54s
Build images / promote (push) Skipped
CI / integration (push) Successful in 2m19s
Milestone #406 phase 2, with issue #3980 folded in. Phase 1 (2026-09-13) unregistered pixiv so nothing could reach it; the code has sat in the tree uncalled since. DeviantArt is why the second half is not left for later — #3069 retired it in code on 2026-08-27 and its stored session was still in the database seven weeks on. Step 5 — the code. Deletes pixiv_client, pixiv_downloader, pixiv_ingester, platforms/pixiv and their three test modules and fixture, then edits out every remaining reference: the dispatch entry, the campaign-id and verify branches in download_backends, the display-name branch in extension_service, and the comments that still described pixiv as live. The consolidation check the step asked for comes back negative: native_ingest_common has seven non-pixiv callers (patreon, subscribestar, membership_reconcile, membership_roster, ingest_core), so nothing there drops to a single user. Step 6 — the data, alembic 0102. Drops pixiv_seen_media and pixiv_failed_media, and deletes credential rows whose platform is not registered. Written as "not registered" rather than "pixiv" at the step's explicit ask, which is what makes one migration cover two retirements: the pixiv OAuth refresh token and DeviantArt's leftover session (#3980). It is also the only way either row can go — the credentials UI renders one card per platform from /api/platforms and looks the credential up by key, so an unregistered platform's row has no card and no Remove button. Pixiv's Source rows are KEPT, changing the milestone's original data table on the operator's call. `platform` is stored only on Source; neither Post nor ImageRecord carries it. Both FKs are ON DELETE SET NULL, so a delete would not lose the art — but it would drop every pixiv image into the gallery's __unsourced__ bucket and strip the platform chip off every pixiv post. The rows stay disabled (0097) and unregistered, so nothing schedules or downloads through them. Keeping them costs nothing and keeps the attribution that "the art already downloaded from pixiv stays" is about. Step 7 — the guard. test_pixiv_code_and_tables_are_gone asserts absence from the module table and from Base.metadata, not from prose (snippet #3352's trap). The extension and registry negative assertions were already in place from phase 1. The final sweep found one real residue step 4 missed: extension/README.md still advertised pixiv support and carried a "Pixiv OAuth" manual-test item. Also replaces the two deleted dispatch tests with one over the whole NATIVE_INGESTER_PLATFORMS set, so adding a platform and forgetting its ingester class now fails at unit level rather than as a mid-download KeyError. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LVjrnpQjRgHdvq95rASoiR
102 lines
3.5 KiB
Python
102 lines
3.5 KiB
Python
import re
|
|
|
|
import pytest
|
|
|
|
from backend.app.services.platforms import (
|
|
PLATFORMS,
|
|
PlatformInfo,
|
|
auth_type_for,
|
|
known_platform_keys,
|
|
to_dict,
|
|
)
|
|
|
|
|
|
def test_known_platform_keys_are_the_supported_four():
|
|
# GS's original five, less pixiv (retired at milestone #406, rule #171).
|
|
assert known_platform_keys() == frozenset({
|
|
"patreon", "subscribestar", "hentaifoundry", "discord",
|
|
})
|
|
|
|
|
|
def test_pixiv_is_retired():
|
|
# Milestone #406 phase 1. Unregistering is what switches pixiv off: the
|
|
# registry feeds /api/platforms, the source validator and the download
|
|
# guard, so this one absence is load-bearing everywhere else.
|
|
assert "pixiv" not in PLATFORMS
|
|
assert "pixiv" not in known_platform_keys()
|
|
|
|
|
|
def test_pixiv_code_and_tables_are_gone():
|
|
"""Milestone #406 phase 2 (step 7). Phase 1 made pixiv unreachable; this is
|
|
the guard that it stays deleted rather than drifting back in.
|
|
|
|
Both halves assert absence from a DATA STRUCTURE — the module table and the
|
|
declarative metadata — not from prose. Snippet #3352's trap is an absence
|
|
check against a comment, which passes the moment someone rewords the
|
|
comment; a re-added module or a re-declared table cannot hide from these.
|
|
|
|
DeviantArt is the reason this exists: #3069 retired it in code on
|
|
2026-08-27 and its credential row was still sitting in the database seven
|
|
weeks later (#3980). A retirement nothing asserts is a retirement that
|
|
half-happens.
|
|
"""
|
|
import importlib
|
|
|
|
from backend.app.models.base import Base
|
|
|
|
for module in (
|
|
"backend.app.services.pixiv_client",
|
|
"backend.app.services.pixiv_downloader",
|
|
"backend.app.services.pixiv_ingester",
|
|
"backend.app.services.platforms.pixiv",
|
|
"backend.app.models.pixiv_seen_media",
|
|
"backend.app.models.pixiv_failed_media",
|
|
):
|
|
with pytest.raises(ModuleNotFoundError):
|
|
importlib.import_module(module)
|
|
|
|
# Alembic 0102 drops these; nothing may re-declare them.
|
|
assert "pixiv_seen_media" not in Base.metadata.tables
|
|
assert "pixiv_failed_media" not in Base.metadata.tables
|
|
|
|
|
|
def test_fanbox_not_in_registry():
|
|
# Sanity check — FC-3a added 'fanbox' by mistake; it's not a GS platform.
|
|
assert "fanbox" not in PLATFORMS
|
|
|
|
|
|
def test_deviantart_is_retired():
|
|
# #3069 executed the 2026-07-05 drop decision (FC downloaders = ART-
|
|
# DEDICATED services only). The registry is what /api/platforms, the
|
|
# source validator and the credential validator all read, so its absence
|
|
# here is what actually retires the platform everywhere else.
|
|
assert "deviantart" not in PLATFORMS
|
|
assert auth_type_for("deviantart") is None
|
|
|
|
|
|
def test_auth_type_for_known_and_unknown():
|
|
assert auth_type_for("patreon") == "cookies"
|
|
assert auth_type_for("discord") == "token"
|
|
assert auth_type_for("nope") is None
|
|
|
|
|
|
def test_each_platform_has_required_fields():
|
|
for key, info in PLATFORMS.items():
|
|
assert isinstance(info, PlatformInfo)
|
|
assert info.key == key
|
|
assert info.name
|
|
assert info.description
|
|
assert info.auth_type in ("cookies", "token")
|
|
assert isinstance(info.requires_auth, bool)
|
|
re.compile(info.url_pattern) # valid regex
|
|
assert info.url_examples
|
|
assert isinstance(info.default_config, dict)
|
|
|
|
|
|
def test_to_dict_round_trip():
|
|
d = to_dict(PLATFORMS["patreon"])
|
|
assert d["key"] == "patreon"
|
|
assert d["auth_type"] == "cookies"
|
|
assert d["default_config"]["sleep"] == 3.0
|
|
assert "url_examples" in d
|