From 37edd844ac840154e5c220ccf365d285a12622af Mon Sep 17 00:00:00 2001 From: wilfordgrimley <2397930+WilfordGrimley@users.noreply.github.com> Date: Fri, 24 Jul 2026 01:42:33 +0000 Subject: [PATCH 1/2] Graduate crash-drill script, hoist test snapshot-sequence pinning Copies the production-verified crash_drill.sh (2026-07-23 DRILL-PASS) into scripts/ops/ and documents it in the resume-contract section of catalog-completion-plan.md. Also fixes the _preserve_shared_factory_sequences convention that broke test_views.py snapshots twice today: instead of every module using a shared factory protecting the one module that asserts on exact sequence-derived values, test_views.py now pins those factories to a fixed baseline per test, so its snapshots are self-determined regardless of suite composition or collection order. Removes the now-redundant per-module fixture + _SHARED_FACTORIES list from the other 31 test files. Full backend suite: 1829 passed, 4 skipped, 0 failed. Refs #153. Co-Authored-By: Claude Fable 5 --- .../tests/__snapshots__/test_views.ambr | 12 +- .../cardpicker/tests/test_artist_votes.py | 21 ---- .../cardpicker/tests/test_artist_writein.py | 13 --- .../cardpicker/tests/test_card_serialise.py | 31 +---- .../tests/test_consensus_impact_report.py | 23 ---- .../tests/test_consensus_recompute.py | 21 ---- .../tests/test_deductive_backfill.py | 26 ----- .../cardpicker/tests/test_harvest_probe.py | 30 +---- .../cardpicker/tests/test_image_cdn_fetch.py | 19 +--- .../cardpicker/tests/test_image_evidence.py | 19 +--- .../cardpicker/tests/test_implicit_votes.py | 29 +---- .../tests/test_local_calculate_verdicts.py | 24 ---- .../tests/test_local_cluster_consistency.py | 32 +----- .../cardpicker/tests/test_local_fallback.py | 20 ---- .../test_local_identify_printing_tags.py | 19 ---- .../tests/test_local_lands_identify.py | 24 ---- .../cardpicker/tests/test_local_phash.py | 17 --- .../tests/test_local_residual_classify.py | 26 ----- .../tests/test_printing_candidates.py | 26 ----- .../tests/test_printing_consensus.py | 24 ---- .../tests/test_printing_consumption_views.py | 25 ---- .../tests/test_printing_metadata_import.py | 20 ---- .../tests/test_printing_tags_views.py | 22 ---- .../tests/test_purge_machine_votes.py | 25 ---- .../cardpicker/tests/test_question_feed.py | 23 ---- .../tests/test_reparse_collector_evidence.py | 22 ---- .../tests/test_resolution_tier_probe.py | 27 +---- .../tests/test_retract_stage_d_by_run_id.py | 23 ---- .../tests/test_search_operator_syntax.py | 23 ---- .../cardpicker/tests/test_tag_votes.py | 29 +---- MPCAutofill/cardpicker/tests/test_tags.py | 28 ----- MPCAutofill/cardpicker/tests/test_views.py | 40 +++---- .../cardpicker/tests/test_vote_queue_views.py | 23 ---- docs/features/catalog-completion-plan.md | 12 ++ docs/lessons.md | 27 +++-- docs/troubleshooting.md | 107 ++++++++---------- docs/upstreaming/extractable-primitives.md | 46 +++++--- docs/upstreaming/readiness-audit.md | 31 ++--- scripts/ops/crash_drill.sh | 94 +++++++++++++++ 39 files changed, 253 insertions(+), 850 deletions(-) create mode 100755 scripts/ops/crash_drill.sh diff --git a/MPCAutofill/cardpicker/tests/__snapshots__/test_views.ambr b/MPCAutofill/cardpicker/tests/__snapshots__/test_views.ambr index 0db9f97a2..ae7a5c36c 100644 --- a/MPCAutofill/cardpicker/tests/__snapshots__/test_views.ambr +++ b/MPCAutofill/cardpicker/tests/__snapshots__/test_views.ambr @@ -193,7 +193,7 @@ 'CARD': dict({ 'Brainstorm': dict({ 'canonicalArtist': dict({ - 'name': 'Artist 102', + 'name': 'Artist 0', }), 'canonicalArtistIsFromVoteOnly': False, 'canonicalArtistSource': 'canonical_card', @@ -451,7 +451,7 @@ 'cards': list([ dict({ 'canonicalArtist': dict({ - 'name': 'Artist 107', + 'name': 'Artist 0', }), 'canonicalArtistIsFromVoteOnly': False, 'canonicalArtistSource': 'canonical_card', @@ -793,7 +793,7 @@ 'cards': list([ dict({ 'canonicalArtist': dict({ - 'name': 'Artist 109', + 'name': 'Artist 0', }), 'canonicalArtistIsFromVoteOnly': False, 'canonicalArtistSource': 'canonical_card', @@ -2792,7 +2792,7 @@ 'cards': list([ dict({ 'canonicalArtist': dict({ - 'name': 'Artist 67', + 'name': 'Artist 0', }), 'canonicalArtistIsFromVoteOnly': False, 'canonicalArtistSource': 'canonical_card', @@ -3136,7 +3136,7 @@ 'cards': list([ dict({ 'canonicalArtist': dict({ - 'name': 'Artist 66', + 'name': 'Artist 0', }), 'canonicalArtistIsFromVoteOnly': False, 'canonicalArtistSource': 'canonical_card', @@ -3579,7 +3579,7 @@ 'cards': list([ dict({ 'canonicalArtist': dict({ - 'name': 'Artist 68', + 'name': 'Artist 0', }), 'canonicalArtistIsFromVoteOnly': False, 'canonicalArtistSource': 'canonical_card', diff --git a/MPCAutofill/cardpicker/tests/test_artist_votes.py b/MPCAutofill/cardpicker/tests/test_artist_votes.py index 420ead23f..5e44db43e 100644 --- a/MPCAutofill/cardpicker/tests/test_artist_votes.py +++ b/MPCAutofill/cardpicker/tests/test_artist_votes.py @@ -15,31 +15,10 @@ from cardpicker.tests.factories import ( CanonicalArtistFactory, CanonicalCardFactory, - CanonicalExpansionFactory, CardArtistVoteFactory, CardFactory, - SourceFactory, ) -# see test_printing_consensus.py for why this capture-and-restore fixture exists -_SHARED_FACTORIES = [ - CardFactory, - SourceFactory, - CanonicalArtistFactory, - CanonicalExpansionFactory, - CanonicalCardFactory, -] - - -@pytest.fixture(autouse=True) -def _preserve_shared_factory_sequences(): - before = {f: f._meta.next_sequence() for f in _SHARED_FACTORIES} - for f, n in before.items(): - f.reset_sequence(n, force=True) - yield - for f, n in before.items(): - f.reset_sequence(n, force=True) - @pytest.fixture(autouse=True) def _clear_rate_limit_cache(): diff --git a/MPCAutofill/cardpicker/tests/test_artist_writein.py b/MPCAutofill/cardpicker/tests/test_artist_writein.py index 2f5a82986..96a9082e4 100644 --- a/MPCAutofill/cardpicker/tests/test_artist_writein.py +++ b/MPCAutofill/cardpicker/tests/test_artist_writein.py @@ -12,19 +12,6 @@ ) from cardpicker.tests.factories import CanonicalArtistFactory, CardFactory -# see test_printing_consensus.py for why this capture-and-restore fixture exists -_SHARED_FACTORIES = [CardFactory, CanonicalArtistFactory] - - -@pytest.fixture(autouse=True) -def _preserve_shared_factory_sequences(): - before = {f: f._meta.next_sequence() for f in _SHARED_FACTORIES} - for f, n in before.items(): - f.reset_sequence(n, force=True) - yield - for f, n in before.items(): - f.reset_sequence(n, force=True) - @pytest.fixture(autouse=True) def _clear_rate_limit_cache(): diff --git a/MPCAutofill/cardpicker/tests/test_card_serialise.py b/MPCAutofill/cardpicker/tests/test_card_serialise.py index 6024da6e2..84c65e601 100644 --- a/MPCAutofill/cardpicker/tests/test_card_serialise.py +++ b/MPCAutofill/cardpicker/tests/test_card_serialise.py @@ -26,9 +26,7 @@ ) from cardpicker.tag_consensus import resolve_and_persist_tag_votes from cardpicker.tests.factories import ( - CanonicalArtistFactory, CanonicalCardFactory, - CanonicalExpansionFactory, CardFactory, CardPrintingTagFactory, CardTagVoteFactory, @@ -36,26 +34,6 @@ TagFactory, ) -# see test_printing_consensus.py for why this capture-and-restore fixture exists -_SHARED_FACTORIES = [ - CardFactory, - SourceFactory, - CanonicalArtistFactory, - CanonicalExpansionFactory, - CanonicalCardFactory, - TagFactory, -] - - -@pytest.fixture(autouse=True) -def _preserve_shared_factory_sequences(): - before = {f: f._meta.next_sequence() for f in _SHARED_FACTORIES} - for f, n in before.items(): - f.reset_sequence(n, force=True) - yield - for f, n in before.items(): - f.reset_sequence(n, force=True) - class TestSuggestedCanonicalCard: def test_none_when_flag_not_set(self, db): @@ -497,11 +475,10 @@ class TestPostExploreSearchBulkPayload: Deliberately self-contained (own `SourceFactory`/`CardFactory` calls, own `search_index --rebuild` call) rather than reusing `test_views.py`'s shared `all_sources`/`all_cards`/`populated_database` fixtures or its `Cards`/`Sources` test - constants - this module's own `_preserve_shared_factory_sequences` autouse fixture (see - top of file) keeps these tests from perturbing that file's sequence-dependent snapshot - assertions, and no snapshot assertion is used here at all (explicit field assertions - only), per the "avoid --snapshot-update against a subset" trap documented in - docs/troubleshooting.md. + constants. `test_views.py`'s own snapshot assertions are pinned to a fixed factory-sequence + baseline per test (see that file's `_pin_shared_factory_sequences` fixture), so this module's + use of the same shared factories can't perturb them - and no snapshot assertion is used here + at all (explicit field assertions only) regardless. """ @staticmethod diff --git a/MPCAutofill/cardpicker/tests/test_consensus_impact_report.py b/MPCAutofill/cardpicker/tests/test_consensus_impact_report.py index ee58269be..7cd37af82 100644 --- a/MPCAutofill/cardpicker/tests/test_consensus_impact_report.py +++ b/MPCAutofill/cardpicker/tests/test_consensus_impact_report.py @@ -6,8 +6,6 @@ test_purge_machine_votes.py's own header comment. """ -import pytest - from django.core.management import call_command from cardpicker.artist_consensus import resolve_and_persist_artist @@ -27,34 +25,13 @@ from cardpicker.tests.factories import ( CanonicalArtistFactory, CanonicalCardFactory, - CanonicalExpansionFactory, CardArtistVoteFactory, CardFactory, CardPrintingTagFactory, CardTagVoteFactory, - SourceFactory, TagFactory, ) -# see test_printing_consensus.py for why this capture-and-restore fixture exists -_SHARED_FACTORIES = [ - CardFactory, - SourceFactory, - CanonicalArtistFactory, - CanonicalExpansionFactory, - CanonicalCardFactory, -] - - -@pytest.fixture(autouse=True) -def _preserve_shared_factory_sequences(): - before = {f: f._meta.next_sequence() for f in _SHARED_FACTORIES} - for f, n in before.items(): - f.reset_sequence(n, force=True) - yield - for f, n in before.items(): - f.reset_sequence(n, force=True) - class TestComputeConsensusImpactReportZeroWrites: def test_performs_no_writes_at_all(self, db): diff --git a/MPCAutofill/cardpicker/tests/test_consensus_recompute.py b/MPCAutofill/cardpicker/tests/test_consensus_recompute.py index c9289865e..e7af59732 100644 --- a/MPCAutofill/cardpicker/tests/test_consensus_recompute.py +++ b/MPCAutofill/cardpicker/tests/test_consensus_recompute.py @@ -25,34 +25,13 @@ from cardpicker.tests.factories import ( CanonicalArtistFactory, CanonicalCardFactory, - CanonicalExpansionFactory, CardArtistVoteFactory, CardFactory, CardPrintingTagFactory, CardTagVoteFactory, - SourceFactory, TagFactory, ) -# see test_consensus_impact_report.py for why this capture-and-restore fixture exists -_SHARED_FACTORIES = [ - CardFactory, - SourceFactory, - CanonicalArtistFactory, - CanonicalExpansionFactory, - CanonicalCardFactory, -] - - -@pytest.fixture(autouse=True) -def _preserve_shared_factory_sequences(): - before = {f: f._meta.next_sequence() for f in _SHARED_FACTORIES} - for f, n in before.items(): - f.reset_sequence(n, force=True) - yield - for f, n in before.items(): - f.reset_sequence(n, force=True) - class TestRunConsensusRecomputeDryRun: def test_dry_run_performs_no_writes(self, db): diff --git a/MPCAutofill/cardpicker/tests/test_deductive_backfill.py b/MPCAutofill/cardpicker/tests/test_deductive_backfill.py index f6787d0d3..bbb9a3fc5 100644 --- a/MPCAutofill/cardpicker/tests/test_deductive_backfill.py +++ b/MPCAutofill/cardpicker/tests/test_deductive_backfill.py @@ -1,5 +1,3 @@ -import pytest - from cardpicker.deductive_backfill import ( DEDUCTIVE_BACKFILL_ANONYMOUS_ID, run_backfill, @@ -10,37 +8,13 @@ from cardpicker.models import PrintingTagStatus, VoteSource from cardpicker.printing_consensus import resolve_printing from cardpicker.tests.factories import ( - CanonicalArtistFactory, CanonicalCardFactory, CanonicalExpansionFactory, CanonicalPrintingMetadataFactory, CardFactory, CardPrintingTagFactory, - SourceFactory, ) -# `factory.Sequence` counters are process-global - see test_printing_consensus.py's identical -# fixture for the full rationale. Mirrored here since this module uses the same shared factories -# - including SourceFactory/CanonicalArtistFactory, consumed indirectly via CardFactory.source -# and CanonicalCardFactory.artist SubFactories, not just the ones referenced by name above. -_SHARED_FACTORIES = [ - CardFactory, - SourceFactory, - CanonicalArtistFactory, - CanonicalExpansionFactory, - CanonicalCardFactory, -] - - -@pytest.fixture(autouse=True) -def _preserve_shared_factory_sequences(): - before = {f: f._meta.next_sequence() for f in _SHARED_FACTORIES} - for f, n in before.items(): - f.reset_sequence(n, force=True) - yield - for f, n in before.items(): - f.reset_sequence(n, force=True) - def _unique_printing(name: str, printings_count: int = 1, **kwargs) -> "CanonicalCardFactory": printing = CanonicalCardFactory(name=name, **kwargs) diff --git a/MPCAutofill/cardpicker/tests/test_harvest_probe.py b/MPCAutofill/cardpicker/tests/test_harvest_probe.py index 4649d8a7b..546180034 100644 --- a/MPCAutofill/cardpicker/tests/test_harvest_probe.py +++ b/MPCAutofill/cardpicker/tests/test_harvest_probe.py @@ -10,35 +10,7 @@ import cardpicker.harvest_probe as module from cardpicker.local_identify_printing_tags import EngineVote, OcrCardResult from cardpicker.models import CardPrintingTag -from cardpicker.tests.factories import ( - CanonicalArtistFactory, - CanonicalCardFactory, - CanonicalExpansionFactory, - CardFactory, - SourceFactory, -) - -# See test_local_lands_identify.py's identical fixture for the full rationale - -# factory.Sequence counters are process-global across the whole pytest run. Includes every -# factory CardFactory/CanonicalCardFactory transitively consume via SubFactory (source, artist, -# expansion) - see docs/troubleshooting.md's "5-6 unrelated test snapshots break" entry. -_SHARED_FACTORIES = [ - CardFactory, - SourceFactory, - CanonicalArtistFactory, - CanonicalExpansionFactory, - CanonicalCardFactory, -] - - -@pytest.fixture(autouse=True) -def _preserve_shared_factory_sequences(): - before = {f: f._meta.next_sequence() for f in _SHARED_FACTORIES} - for f, n in before.items(): - f.reset_sequence(n, force=True) - yield - for f, n in before.items(): - f.reset_sequence(n, force=True) +from cardpicker.tests.factories import CanonicalCardFactory, CardFactory class FakeImage: diff --git a/MPCAutofill/cardpicker/tests/test_image_cdn_fetch.py b/MPCAutofill/cardpicker/tests/test_image_cdn_fetch.py index 6bf136f35..8a84fecd8 100644 --- a/MPCAutofill/cardpicker/tests/test_image_cdn_fetch.py +++ b/MPCAutofill/cardpicker/tests/test_image_cdn_fetch.py @@ -10,24 +10,7 @@ import cardpicker.image_cdn_fetch as module from cardpicker.harvest_fetch_limiter import GoogleFetchLockoutError -from cardpicker.tests.factories import ( - CanonicalArtistFactory, - CanonicalExpansionFactory, - CardFactory, - SourceFactory, -) - -_SHARED_FACTORIES = [CardFactory, SourceFactory, CanonicalArtistFactory, CanonicalExpansionFactory] - - -@pytest.fixture(autouse=True) -def _preserve_shared_factory_sequences(): - before = {f: f._meta.next_sequence() for f in _SHARED_FACTORIES} - for f, n in before.items(): - f.reset_sequence(n, force=True) - yield - for f, n in before.items(): - f.reset_sequence(n, force=True) +from cardpicker.tests.factories import CardFactory class TestFetchCardImageLockoutCarveOut: diff --git a/MPCAutofill/cardpicker/tests/test_image_evidence.py b/MPCAutofill/cardpicker/tests/test_image_evidence.py index 94c159751..b15544cba 100644 --- a/MPCAutofill/cardpicker/tests/test_image_evidence.py +++ b/MPCAutofill/cardpicker/tests/test_image_evidence.py @@ -93,14 +93,7 @@ from cardpicker.local_ocr import DEFAULT_CROP_BOX, LEGAL_LINE_CROP_BOX from cardpicker.local_phash import ART_CROP_BOX from cardpicker.models import CardScanLog, ImageEvidence -from cardpicker.tests.factories import ( - CanonicalArtistFactory, - CanonicalExpansionFactory, - CardFactory, - SourceFactory, -) - -_SHARED_FACTORIES = [CardFactory, SourceFactory, CanonicalArtistFactory, CanonicalExpansionFactory] +from cardpicker.tests.factories import CardFactory @dataclass(frozen=True) @@ -204,16 +197,6 @@ def _build_card_image( return img -@pytest.fixture(autouse=True) -def _preserve_shared_factory_sequences(): - before = {f: f._meta.next_sequence() for f in _SHARED_FACTORIES} - for f, n in before.items(): - f.reset_sequence(n, force=True) - yield - for f, n in before.items(): - f.reset_sequence(n, force=True) - - class TestExtractCardEvidence: def test_successful_fetch_marks_fetch_ok_and_records_no_skip(self, db, monkeypatch): card = CardFactory(content_phash=12345) diff --git a/MPCAutofill/cardpicker/tests/test_implicit_votes.py b/MPCAutofill/cardpicker/tests/test_implicit_votes.py index 03fd034d0..b8087e1f5 100644 --- a/MPCAutofill/cardpicker/tests/test_implicit_votes.py +++ b/MPCAutofill/cardpicker/tests/test_implicit_votes.py @@ -12,40 +12,13 @@ VoteSource, ) from cardpicker.tag_consensus import resolve_and_persist_tag_votes -from cardpicker.tests.factories import ( - CanonicalArtistFactory, - CanonicalCardFactory, - CanonicalExpansionFactory, - CardFactory, - CardTagVoteFactory, - SourceFactory, - TagFactory, -) +from cardpicker.tests.factories import CardFactory, CardTagVoteFactory, TagFactory from cardpicker.views import ( IMPLICIT_VOTE_SURFACE, _cast_implicit_vote_and_resolve, _retract_implicit_vote_and_resolve, ) -# see test_printing_consensus.py for why this capture-and-restore fixture exists -_SHARED_FACTORIES = [ - CardFactory, - SourceFactory, - CanonicalArtistFactory, - CanonicalExpansionFactory, - CanonicalCardFactory, -] - - -@pytest.fixture(autouse=True) -def _preserve_shared_factory_sequences(): - before = {f: f._meta.next_sequence() for f in _SHARED_FACTORIES} - for f, n in before.items(): - f.reset_sequence(n, force=True) - yield - for f, n in before.items(): - f.reset_sequence(n, force=True) - @pytest.fixture(autouse=True) def _clear_rate_limit_cache(): diff --git a/MPCAutofill/cardpicker/tests/test_local_calculate_verdicts.py b/MPCAutofill/cardpicker/tests/test_local_calculate_verdicts.py index 6030ef3cc..fbd689b47 100644 --- a/MPCAutofill/cardpicker/tests/test_local_calculate_verdicts.py +++ b/MPCAutofill/cardpicker/tests/test_local_calculate_verdicts.py @@ -73,14 +73,12 @@ VoteSource, ) from cardpicker.tests.factories import ( - CanonicalArtistFactory, CanonicalCardFactory, CanonicalExpansionFactory, CanonicalPrintingMetadataFactory, CardFactory, DFCPairFactory, ImageEvidenceFactory, - SourceFactory, ) from cardpicker.utils import twos_complement @@ -100,28 +98,6 @@ def _dfc_record(**overrides: Any) -> dict[str, Any]: return base -# see test_local_identify_printing_tags.py's identical fixture for the full rationale - -# factory.Sequence counters are process-global across the whole pytest run. -_SHARED_FACTORIES = [ - CardFactory, - SourceFactory, - CanonicalArtistFactory, - CanonicalExpansionFactory, - CanonicalCardFactory, - DFCPairFactory, -] - - -@pytest.fixture(autouse=True) -def _preserve_shared_factory_sequences(): - before = {f: f._meta.next_sequence() for f in _SHARED_FACTORIES} - for f, n in before.items(): - f.reset_sequence(n, force=True) - yield - for f, n in before.items(): - f.reset_sequence(n, force=True) - - def _hash_of(expansion_code: str) -> int: image = render_set_symbol(expansion_code) assert image is not None diff --git a/MPCAutofill/cardpicker/tests/test_local_cluster_consistency.py b/MPCAutofill/cardpicker/tests/test_local_cluster_consistency.py index 146b62684..ed61efa52 100644 --- a/MPCAutofill/cardpicker/tests/test_local_cluster_consistency.py +++ b/MPCAutofill/cardpicker/tests/test_local_cluster_consistency.py @@ -7,37 +7,7 @@ from cardpicker.local_cluster_consistency import find_cluster_printing_divergences from cardpicker.models import PrintingTagStatus -from cardpicker.tests.factories import ( - CanonicalArtistFactory, - CanonicalCardFactory, - CanonicalExpansionFactory, - CardFactory, - SourceFactory, -) - -# See test_local_lands_identify.py's identical fixture for the full rationale - -# factory.Sequence counters are process-global across the whole pytest run. Includes every -# factory CardFactory/CanonicalCardFactory transitively consume via SubFactory (source, artist, -# expansion) - not just the two factories this file calls directly - per -# docs/troubleshooting.md's "5-6 unrelated test snapshots break" entry, which calls out this -# exact under-listing mistake as a recurring one. -_SHARED_FACTORIES = [ - CardFactory, - SourceFactory, - CanonicalArtistFactory, - CanonicalExpansionFactory, - CanonicalCardFactory, -] - - -@pytest.fixture(autouse=True) -def _preserve_shared_factory_sequences(): - before = {f: f._meta.next_sequence() for f in _SHARED_FACTORIES} - for f, n in before.items(): - f.reset_sequence(n, force=True) - yield - for f, n in before.items(): - f.reset_sequence(n, force=True) +from cardpicker.tests.factories import CanonicalCardFactory, CardFactory def _resolved_card(content_phash, printing): diff --git a/MPCAutofill/cardpicker/tests/test_local_fallback.py b/MPCAutofill/cardpicker/tests/test_local_fallback.py index 7dad9a3ba..6765206ba 100644 --- a/MPCAutofill/cardpicker/tests/test_local_fallback.py +++ b/MPCAutofill/cardpicker/tests/test_local_fallback.py @@ -12,7 +12,6 @@ this suite. """ -import pytest from PIL import Image, ImageDraw import cardpicker.local_ocr as local_ocr @@ -43,28 +42,9 @@ CanonicalExpansionFactory, CanonicalPrintingMetadataFactory, CardFactory, - SourceFactory, TagFactory, ) -_SHARED_FACTORIES = [ - CardFactory, - SourceFactory, - CanonicalArtistFactory, - CanonicalExpansionFactory, - CanonicalCardFactory, -] - - -@pytest.fixture(autouse=True) -def _preserve_shared_factory_sequences(): - before = {f: f._meta.next_sequence() for f in _SHARED_FACTORIES} - for f, n in before.items(): - f.reset_sequence(n, force=True) - yield - for f, n in before.items(): - f.reset_sequence(n, force=True) - class TestExtractArtistName: def test_standard_illus_prefix(self): diff --git a/MPCAutofill/cardpicker/tests/test_local_identify_printing_tags.py b/MPCAutofill/cardpicker/tests/test_local_identify_printing_tags.py index 1d5d4012a..b48ca0421 100644 --- a/MPCAutofill/cardpicker/tests/test_local_identify_printing_tags.py +++ b/MPCAutofill/cardpicker/tests/test_local_identify_printing_tags.py @@ -74,25 +74,6 @@ TagFactory, ) -# see test_deductive_backfill.py's identical fixture for the full rationale -_SHARED_FACTORIES = [ - CardFactory, - SourceFactory, - CanonicalArtistFactory, - CanonicalExpansionFactory, - CanonicalCardFactory, -] - - -@pytest.fixture(autouse=True) -def _preserve_shared_factory_sequences(): - before = {f: f._meta.next_sequence() for f in _SHARED_FACTORIES} - for f, n in before.items(): - f.reset_sequence(n, force=True) - yield - for f, n in before.items(): - f.reset_sequence(n, force=True) - class TestSelection: def test_excludes_resolved_cards(self, db): diff --git a/MPCAutofill/cardpicker/tests/test_local_lands_identify.py b/MPCAutofill/cardpicker/tests/test_local_lands_identify.py index 79e1098f1..e69d0bba9 100644 --- a/MPCAutofill/cardpicker/tests/test_local_lands_identify.py +++ b/MPCAutofill/cardpicker/tests/test_local_lands_identify.py @@ -5,8 +5,6 @@ mocked exactly like test_local_residual_classify.py mocks the same functions. """ -import pytest - import cardpicker.local_lands_identify as module from cardpicker.local_identify_printing_tags import ( OCR_ANONYMOUS_ID, @@ -29,31 +27,9 @@ from cardpicker.tests.factories import ( CanonicalArtistFactory, CanonicalCardFactory, - CanonicalExpansionFactory, CardFactory, - SourceFactory, ) -# See test_local_identify_printing_tags.py's identical fixture for the full rationale - -# factory.Sequence counters are process-global across the whole pytest run. -_SHARED_FACTORIES = [ - CardFactory, - SourceFactory, - CanonicalArtistFactory, - CanonicalExpansionFactory, - CanonicalCardFactory, -] - - -@pytest.fixture(autouse=True) -def _preserve_shared_factory_sequences(): - before = {f: f._meta.next_sequence() for f in _SHARED_FACTORIES} - for f, n in before.items(): - f.reset_sequence(n, force=True) - yield - for f, n in before.items(): - f.reset_sequence(n, force=True) - class TestIsLandsTarget: def test_basic_land_name_is_a_target_regardless_of_candidate_count(self): diff --git a/MPCAutofill/cardpicker/tests/test_local_phash.py b/MPCAutofill/cardpicker/tests/test_local_phash.py index 152808eab..b1b414bc8 100644 --- a/MPCAutofill/cardpicker/tests/test_local_phash.py +++ b/MPCAutofill/cardpicker/tests/test_local_phash.py @@ -7,29 +7,12 @@ cover the local-first/REST-fallback ordering that change adds. """ -import pytest - import cardpicker.local_phash as module from cardpicker.tests.factories import ( - CanonicalArtistFactory, CanonicalCardFactory, - CanonicalExpansionFactory, CanonicalPrintingMetadataFactory, ) -# See test_harvest_probe.py's identical fixture for the full rationale. -_SHARED_FACTORIES = [CanonicalArtistFactory, CanonicalExpansionFactory, CanonicalCardFactory] - - -@pytest.fixture(autouse=True) -def _preserve_shared_factory_sequences(): - before = {f: f._meta.next_sequence() for f in _SHARED_FACTORIES} - for f, n in before.items(): - f.reset_sequence(n, force=True) - yield - for f, n in before.items(): - f.reset_sequence(n, force=True) - class TestLocalArtCropUrl: def test_returns_url_when_metadata_present_and_populated(self, db): diff --git a/MPCAutofill/cardpicker/tests/test_local_residual_classify.py b/MPCAutofill/cardpicker/tests/test_local_residual_classify.py index 038936cf2..932b5bd1b 100644 --- a/MPCAutofill/cardpicker/tests/test_local_residual_classify.py +++ b/MPCAutofill/cardpicker/tests/test_local_residual_classify.py @@ -6,8 +6,6 @@ fetch_card_image/run_ocr_for_card. """ -import pytest - import cardpicker.local_residual_classify as module from cardpicker.local_fallback import FALLBACK_ANONYMOUS_ID, FallbackOutcome from cardpicker.local_identify_printing_tags import ( @@ -40,35 +38,11 @@ from cardpicker.tests.factories import ( CanonicalArtistFactory, CanonicalCardFactory, - CanonicalExpansionFactory, CardArtistVoteFactory, CardFactory, - SourceFactory, TagFactory, ) -# see test_local_identify_printing_tags.py's identical fixture for the full rationale - -# factory.Sequence counters are process-global across the whole pytest run, so a new test file -# using these shared factories shifts snapshot-style assertions elsewhere (e.g. -# test_views.py::TestGetTags) unless the sequence is captured/restored around this file's tests. -_SHARED_FACTORIES = [ - CardFactory, - SourceFactory, - CanonicalArtistFactory, - CanonicalExpansionFactory, - CanonicalCardFactory, -] - - -@pytest.fixture(autouse=True) -def _preserve_shared_factory_sequences(): - before = {f: f._meta.next_sequence() for f in _SHARED_FACTORIES} - for f, n in before.items(): - f.reset_sequence(n, force=True) - yield - for f, n in before.items(): - f.reset_sequence(n, force=True) - class TestRecoverFrameMismatchPrintingViaPhash: def test_recovers_matching_candidate(self, db): diff --git a/MPCAutofill/cardpicker/tests/test_printing_candidates.py b/MPCAutofill/cardpicker/tests/test_printing_candidates.py index 48b6a6ec4..241f70596 100644 --- a/MPCAutofill/cardpicker/tests/test_printing_candidates.py +++ b/MPCAutofill/cardpicker/tests/test_printing_candidates.py @@ -1,43 +1,17 @@ import datetime as dt -import pytest - from cardpicker.printing_candidates import ( find_candidates_by_name, get_ranked_printing_candidates, rank_candidates_by_confidence, ) from cardpicker.tests.factories import ( - CanonicalArtistFactory, CanonicalCardFactory, CanonicalExpansionFactory, CanonicalPrintingMetadataFactory, CardFactory, - SourceFactory, ) -# `factory.Sequence` counters are process-global, and some other test modules' snapshot -# assertions hardcode exact sequence-derived values (e.g. "Artist 0"). Capture-and-restore -# keeps this module's use of these shared factories invisible to the rest of the suite, -# regardless of test collection order. -_SHARED_FACTORIES = [ - CardFactory, - SourceFactory, - CanonicalArtistFactory, - CanonicalExpansionFactory, - CanonicalCardFactory, -] - - -@pytest.fixture(autouse=True) -def _preserve_shared_factory_sequences(): - before = {f: f._meta.next_sequence() for f in _SHARED_FACTORIES} - for f, n in before.items(): - f.reset_sequence(n, force=True) - yield - for f, n in before.items(): - f.reset_sequence(n, force=True) - class TestFindCandidatesByName: def test_matches_regardless_of_punctuation(self, db): diff --git a/MPCAutofill/cardpicker/tests/test_printing_consensus.py b/MPCAutofill/cardpicker/tests/test_printing_consensus.py index f9e2f8cd2..b693692c0 100644 --- a/MPCAutofill/cardpicker/tests/test_printing_consensus.py +++ b/MPCAutofill/cardpicker/tests/test_printing_consensus.py @@ -11,38 +11,14 @@ resolve_printing, ) from cardpicker.tests.factories import ( - CanonicalArtistFactory, CanonicalCardFactory, CanonicalExpansionFactory, CanonicalPrintingMetadataFactory, CardFactory, CardPrintingTagFactory, - SourceFactory, ) from cardpicker.vote_consensus import DEDUCTIVE_BACKFILL_ANONYMOUS_ID -# `factory.Sequence` counters are process-global, and some other test modules' -# snapshot assertions hardcode exact sequence-derived values (e.g. "Artist 0"). -# Capture-and-restore keeps this module's use of these shared factories invisible -# to the rest of the suite, regardless of test collection order. -_SHARED_FACTORIES = [ - CardFactory, - SourceFactory, - CanonicalArtistFactory, - CanonicalExpansionFactory, - CanonicalCardFactory, -] - - -@pytest.fixture(autouse=True) -def _preserve_shared_factory_sequences(): - before = {f: f._meta.next_sequence() for f in _SHARED_FACTORIES} - for f, n in before.items(): - f.reset_sequence(n, force=True) - yield - for f, n in before.items(): - f.reset_sequence(n, force=True) - class TestResolvePrinting: def test_no_votes_returns_none(self, db): diff --git a/MPCAutofill/cardpicker/tests/test_printing_consumption_views.py b/MPCAutofill/cardpicker/tests/test_printing_consumption_views.py index b6d7e925f..4ab04a0af 100644 --- a/MPCAutofill/cardpicker/tests/test_printing_consumption_views.py +++ b/MPCAutofill/cardpicker/tests/test_printing_consumption_views.py @@ -16,37 +16,12 @@ from cardpicker import views from cardpicker.models import PrintingTagStatus from cardpicker.tests.factories import ( - CanonicalArtistFactory, CanonicalCardFactory, CanonicalExpansionFactory, CanonicalPrintingMetadataFactory, CardFactory, - SourceFactory, ) -# `factory.Sequence` counters are process-global across the whole pytest session - a fresh -# test file using these shared factories shifts sequence-derived values (e.g. "Artist 0") -# that other test files' snapshots hardcode, purely based on collection order. See -# test_printing_consensus.py's identical fixture and docs/lessons.md for the full story. -_SHARED_FACTORIES = [ - CardFactory, - SourceFactory, - CanonicalArtistFactory, - CanonicalExpansionFactory, - CanonicalCardFactory, -] - - -@pytest.fixture(autouse=True) -def _preserve_shared_factory_sequences(): - before = {f: f._meta.next_sequence() for f in _SHARED_FACTORIES} - for f, n in before.items(): - f.reset_sequence(n, force=True) - yield - for f, n in before.items(): - f.reset_sequence(n, force=True) - - BASE_SEARCH_SETTINGS = { "searchTypeSettings": {"fuzzySearch": False, "filterCardbacks": False}, "sourceSettings": {"sources": []}, # populated per-test with the actual source pk diff --git a/MPCAutofill/cardpicker/tests/test_printing_metadata_import.py b/MPCAutofill/cardpicker/tests/test_printing_metadata_import.py index 9e3257a9d..300e07aba 100644 --- a/MPCAutofill/cardpicker/tests/test_printing_metadata_import.py +++ b/MPCAutofill/cardpicker/tests/test_printing_metadata_import.py @@ -3,8 +3,6 @@ from pathlib import Path from typing import Any -import pytest - from cardpicker.models import CanonicalPrintingMetadata from cardpicker.printing_metadata_import import ( _load_back_face_names, @@ -13,28 +11,10 @@ is_back_face, ) from cardpicker.tests.factories import ( - CanonicalArtistFactory, CanonicalCardFactory, - CanonicalExpansionFactory, CanonicalPrintingMetadataFactory, ) -# `factory.Sequence` counters are process-global, and some other test modules' -# snapshot assertions hardcode exact sequence-derived values (e.g. "Artist 0"). -# Capture-and-restore keeps this module's use of these shared factories invisible -# to the rest of the suite, regardless of test collection order. -_SHARED_FACTORIES = [CanonicalArtistFactory, CanonicalExpansionFactory, CanonicalCardFactory] - - -@pytest.fixture(autouse=True) -def _preserve_shared_factory_sequences(): - before = {f: f._meta.next_sequence() for f in _SHARED_FACTORIES} - for f, n in before.items(): - f.reset_sequence(n, force=True) - yield - for f, n in before.items(): - f.reset_sequence(n, force=True) - def _write_bulk_data_file(tmp_path: Path, records: list[dict[str, Any]]) -> Path: path = tmp_path / "default_cards.json" diff --git a/MPCAutofill/cardpicker/tests/test_printing_tags_views.py b/MPCAutofill/cardpicker/tests/test_printing_tags_views.py index c4df66555..bb386efd6 100644 --- a/MPCAutofill/cardpicker/tests/test_printing_tags_views.py +++ b/MPCAutofill/cardpicker/tests/test_printing_tags_views.py @@ -6,34 +6,12 @@ from cardpicker import views from cardpicker.models import CardPrintingTag, PrintingTagStatus, VoteSource from cardpicker.tests.factories import ( - CanonicalArtistFactory, CanonicalCardFactory, - CanonicalExpansionFactory, CanonicalPrintingMetadataFactory, CardFactory, CardPrintingTagFactory, - SourceFactory, ) -# see test_printing_consensus.py for why this capture-and-restore fixture exists -_SHARED_FACTORIES = [ - CardFactory, - SourceFactory, - CanonicalArtistFactory, - CanonicalExpansionFactory, - CanonicalCardFactory, -] - - -@pytest.fixture(autouse=True) -def _preserve_shared_factory_sequences(): - before = {f: f._meta.next_sequence() for f in _SHARED_FACTORIES} - for f, n in before.items(): - f.reset_sequence(n, force=True) - yield - for f, n in before.items(): - f.reset_sequence(n, force=True) - @pytest.fixture(autouse=True) def _clear_rate_limit_cache(): diff --git a/MPCAutofill/cardpicker/tests/test_purge_machine_votes.py b/MPCAutofill/cardpicker/tests/test_purge_machine_votes.py index efb445f40..80d01195a 100644 --- a/MPCAutofill/cardpicker/tests/test_purge_machine_votes.py +++ b/MPCAutofill/cardpicker/tests/test_purge_machine_votes.py @@ -6,8 +6,6 @@ arithmetic in these tests matches what a real purge would actually do in production. """ -import pytest - from django.core.management import call_command from cardpicker.management.commands.purge_machine_votes import ( @@ -29,31 +27,8 @@ CardArtistVoteFactory, CardFactory, CardPrintingTagFactory, - SourceFactory, ) -# see test_printing_consensus.py for why this capture-and-restore fixture exists - this file's -# own CanonicalArtistFactory/CanonicalCardFactory/etc. calls would otherwise shift the shared -# sequence counters that unrelated snapshot tests elsewhere (e.g. test_views.py) hardcode exact -# values for. -_SHARED_FACTORIES = [ - CardFactory, - SourceFactory, - CanonicalArtistFactory, - CanonicalExpansionFactory, - CanonicalCardFactory, -] - - -@pytest.fixture(autouse=True) -def _preserve_shared_factory_sequences(): - before = {f: f._meta.next_sequence() for f in _SHARED_FACTORIES} - for f, n in before.items(): - f.reset_sequence(n, force=True) - yield - for f, n in before.items(): - f.reset_sequence(n, force=True) - class TestPurgeRun: def test_dry_run_counts_without_deleting_anything(self, db): diff --git a/MPCAutofill/cardpicker/tests/test_question_feed.py b/MPCAutofill/cardpicker/tests/test_question_feed.py index daf95819c..6489d1446 100644 --- a/MPCAutofill/cardpicker/tests/test_question_feed.py +++ b/MPCAutofill/cardpicker/tests/test_question_feed.py @@ -1,5 +1,3 @@ -import pytest - from django.urls import reverse from cardpicker import views @@ -21,34 +19,13 @@ from cardpicker.tests.factories import ( CanonicalArtistFactory, CanonicalCardFactory, - CanonicalExpansionFactory, CardArtistVoteFactory, CardFactory, CardPrintingTagFactory, CardTagVoteFactory, - SourceFactory, TagFactory, ) -# see test_printing_consensus.py for why this capture-and-restore fixture exists -_SHARED_FACTORIES = [ - CardFactory, - SourceFactory, - CanonicalArtistFactory, - CanonicalExpansionFactory, - CanonicalCardFactory, -] - - -@pytest.fixture(autouse=True) -def _preserve_shared_factory_sequences(): - before = {f: f._meta.next_sequence() for f in _SHARED_FACTORIES} - for f, n in before.items(): - f.reset_sequence(n, force=True) - yield - for f, n in before.items(): - f.reset_sequence(n, force=True) - def make_ai_suggested_card(anonymous_id: str = "ai-bot") -> tuple: card = CardFactory(printing_tag_status=PrintingTagStatus.UNRESOLVED) diff --git a/MPCAutofill/cardpicker/tests/test_reparse_collector_evidence.py b/MPCAutofill/cardpicker/tests/test_reparse_collector_evidence.py index 9f8986d50..f5b4f2cc7 100644 --- a/MPCAutofill/cardpicker/tests/test_reparse_collector_evidence.py +++ b/MPCAutofill/cardpicker/tests/test_reparse_collector_evidence.py @@ -33,35 +33,13 @@ ) from cardpicker.printing_consensus import resolve_and_persist_printing from cardpicker.tests.factories import ( - CanonicalArtistFactory, CanonicalCardFactory, CanonicalExpansionFactory, CardFactory, CardPrintingTagFactory, ImageEvidenceFactory, - SourceFactory, ) -# see test_local_calculate_verdicts.py's identical fixture for the full rationale - -# factory.Sequence counters are process-global across the whole pytest run. -_SHARED_FACTORIES = [ - CardFactory, - SourceFactory, - CanonicalArtistFactory, - CanonicalExpansionFactory, - CanonicalCardFactory, -] - - -@pytest.fixture(autouse=True) -def _preserve_shared_factory_sequences(): - before = {f: f._meta.next_sequence() for f in _SHARED_FACTORIES} - for f, n in before.items(): - f.reset_sequence(n, force=True) - yield - for f, n in before.items(): - f.reset_sequence(n, force=True) - def _evidence(card, **overrides): """Same shape as test_local_calculate_verdicts.py's own `_evidence` helper - deliberately diff --git a/MPCAutofill/cardpicker/tests/test_resolution_tier_probe.py b/MPCAutofill/cardpicker/tests/test_resolution_tier_probe.py index 624cd49e3..aade2307c 100644 --- a/MPCAutofill/cardpicker/tests/test_resolution_tier_probe.py +++ b/MPCAutofill/cardpicker/tests/test_resolution_tier_probe.py @@ -9,32 +9,7 @@ import cardpicker.resolution_tier_probe as module from cardpicker.local_identify_printing_tags import EngineVote, OcrCardResult -from cardpicker.tests.factories import ( - CanonicalArtistFactory, - CanonicalCardFactory, - CanonicalExpansionFactory, - CardFactory, - SourceFactory, -) - -# See test_harvest_probe.py's identical fixture for the full rationale. -_SHARED_FACTORIES = [ - CardFactory, - SourceFactory, - CanonicalArtistFactory, - CanonicalExpansionFactory, - CanonicalCardFactory, -] - - -@pytest.fixture(autouse=True) -def _preserve_shared_factory_sequences(): - before = {f: f._meta.next_sequence() for f in _SHARED_FACTORIES} - for f, n in before.items(): - f.reset_sequence(n, force=True) - yield - for f, n in before.items(): - f.reset_sequence(n, force=True) +from cardpicker.tests.factories import CanonicalCardFactory, CardFactory class TestRunResolutionTierProbe: diff --git a/MPCAutofill/cardpicker/tests/test_retract_stage_d_by_run_id.py b/MPCAutofill/cardpicker/tests/test_retract_stage_d_by_run_id.py index 7f636201c..05ad92a7b 100644 --- a/MPCAutofill/cardpicker/tests/test_retract_stage_d_by_run_id.py +++ b/MPCAutofill/cardpicker/tests/test_retract_stage_d_by_run_id.py @@ -29,34 +29,11 @@ ) from cardpicker.printing_consensus import resolve_and_persist_printing from cardpicker.tests.factories import ( - CanonicalArtistFactory, CanonicalCardFactory, - CanonicalExpansionFactory, CardFactory, CardPrintingTagFactory, - SourceFactory, ) -# see test_reparse_collector_evidence.py's identical fixture for the full rationale - -# factory.Sequence counters are process-global across the whole pytest run. -_SHARED_FACTORIES = [ - CardFactory, - SourceFactory, - CanonicalArtistFactory, - CanonicalExpansionFactory, - CanonicalCardFactory, -] - - -@pytest.fixture(autouse=True) -def _preserve_shared_factory_sequences(): - before = {f: f._meta.next_sequence() for f in _SHARED_FACTORIES} - for f, n in before.items(): - f.reset_sequence(n, force=True) - yield - for f, n in before.items(): - f.reset_sequence(n, force=True) - class TestRetractRunId: def test_dry_run_counts_without_deleting_anything(self, db): diff --git a/MPCAutofill/cardpicker/tests/test_search_operator_syntax.py b/MPCAutofill/cardpicker/tests/test_search_operator_syntax.py index 646382ab1..50e017f8a 100644 --- a/MPCAutofill/cardpicker/tests/test_search_operator_syntax.py +++ b/MPCAutofill/cardpicker/tests/test_search_operator_syntax.py @@ -22,31 +22,8 @@ CanonicalExpansionFactory, CanonicalPrintingMetadataFactory, CardFactory, - SourceFactory, ) -# `factory.Sequence` counters are process-global across the whole pytest session - a fresh test -# file using these shared factories shifts sequence-derived values that other test files' -# snapshots hardcode, purely based on collection order. See -# test_printing_consumption_views.py's identical fixture and docs/lessons.md for the full story. -_SHARED_FACTORIES = [ - CardFactory, - SourceFactory, - CanonicalArtistFactory, - CanonicalExpansionFactory, - CanonicalCardFactory, -] - - -@pytest.fixture(autouse=True) -def _preserve_shared_factory_sequences(): - before = {f: f._meta.next_sequence() for f in _SHARED_FACTORIES} - for f, n in before.items(): - f.reset_sequence(n, force=True) - yield - for f, n in before.items(): - f.reset_sequence(n, force=True) - def search_settings_for(source_pk: int, **filter_overrides) -> dict: return { diff --git a/MPCAutofill/cardpicker/tests/test_tag_votes.py b/MPCAutofill/cardpicker/tests/test_tag_votes.py index 567bd852c..320a38f71 100644 --- a/MPCAutofill/cardpicker/tests/test_tag_votes.py +++ b/MPCAutofill/cardpicker/tests/test_tag_votes.py @@ -21,34 +21,7 @@ resolve_and_persist_tag_votes, resolve_tag, ) -from cardpicker.tests.factories import ( - CanonicalArtistFactory, - CanonicalCardFactory, - CanonicalExpansionFactory, - CardFactory, - CardTagVoteFactory, - SourceFactory, - TagFactory, -) - -# see test_printing_consensus.py for why this capture-and-restore fixture exists -_SHARED_FACTORIES = [ - CardFactory, - SourceFactory, - CanonicalArtistFactory, - CanonicalExpansionFactory, - CanonicalCardFactory, -] - - -@pytest.fixture(autouse=True) -def _preserve_shared_factory_sequences(): - before = {f: f._meta.next_sequence() for f in _SHARED_FACTORIES} - for f, n in before.items(): - f.reset_sequence(n, force=True) - yield - for f, n in before.items(): - f.reset_sequence(n, force=True) +from cardpicker.tests.factories import CardFactory, CardTagVoteFactory, TagFactory @pytest.fixture(autouse=True) diff --git a/MPCAutofill/cardpicker/tests/test_tags.py b/MPCAutofill/cardpicker/tests/test_tags.py index e904e1e08..980df34ac 100644 --- a/MPCAutofill/cardpicker/tests/test_tags.py +++ b/MPCAutofill/cardpicker/tests/test_tags.py @@ -1,40 +1,12 @@ -import pytest - from cardpicker.models import Tag, TagAliasSuggestion, TagSuggestionStatus from cardpicker.search.sanitisation import fix_whitespace from cardpicker.tags import Tags from cardpicker.tests.factories import ( - CanonicalArtistFactory, CanonicalCardFactory, CanonicalExpansionFactory, - CardFactory, - SourceFactory, TagFactory, ) -# `factory.Sequence` counters are process-global, and some other test modules' snapshot -# assertions hardcode exact sequence-derived values (e.g. "Artist 0"). Capture-and-restore -# keeps this module's use of these shared factories invisible to the rest of the suite, -# regardless of test collection order. -_SHARED_FACTORIES = [ - CardFactory, - SourceFactory, - CanonicalArtistFactory, - CanonicalExpansionFactory, - CanonicalCardFactory, - TagFactory, -] - - -@pytest.fixture(autouse=True) -def _preserve_shared_factory_sequences(): - before = {f: f._meta.next_sequence() for f in _SHARED_FACTORIES} - for f, n in before.items(): - f.reset_sequence(n, force=True) - yield - for f, n in before.items(): - f.reset_sequence(n, force=True) - class TestMatchTagFuzzy: def test_high_confidence_match(self, db, settings): diff --git a/MPCAutofill/cardpicker/tests/test_views.py b/MPCAutofill/cardpicker/tests/test_views.py index 865daad0a..53f8458e7 100644 --- a/MPCAutofill/cardpicker/tests/test_views.py +++ b/MPCAutofill/cardpicker/tests/test_views.py @@ -20,22 +20,19 @@ TagFactory, ) -# see test_local_identify_printing_tags.py's identical fixture for the full rationale - -# factory.Sequence counters are process-global across the whole pytest run, so any test using -# `populated_database` (function-scoped, recreates all_sources/all_cards fresh every call) -# permanently shifts sequence-derived snapshot values for every test that runs after it in this -# file (TestGetSampleCards, TestNewCardsFirstPages, TestNewCardsPage, TestPostExploreSearchResults -# all hardcode artist names like "Artist 102"). Only the one new E-2 test below needs insulating - -# existing tests in this file rely on the ambient cumulative count and must not be touched. -# -# Scoping this to a single test is non-trivial: `TestPostEditorSearchResults`'s own -# `autouse_populated_database` (class-scoped autouse) always instantiates before a same-scope -# fixture the test merely *requests* - by the time a normal requested fixture's body runs, -# `populated_database` has already consumed its sequence numbers, so a naive capture-before/ -# restore-after fixture requested by the test is too late to see the true "before" value. -# Module-level autouse fixtures instantiate before class-level autouse fixtures even at the same -# effective (function) scope, so this is deliberately autouse=True at module level - but it is a -# no-op for every test except the one named below, so it has zero effect on the rest of this file. +# `factory.Sequence` counters are process-global for the whole pytest run - unpinned, this +# module's snapshots (`__snapshots__/test_views.ambr`) would embed sequence-derived values (e.g. +# an autogenerated "Artist N" name, via `brainstorm_canonical_card`'s default +# `CanonicalCardFactory`/`CanonicalArtistFactory` SubFactory chain) that silently depend on total +# call count up to that point in collection order - both across the whole suite AND across this +# file's own earlier tests. Rather than tracking that ambient count (the old approach: every +# *other* file using these shared factories had to capture/restore its own use of them around +# itself so this file's hardcoded values stayed put - fragile, forgotten repeatedly, see +# docs/troubleshooting.md's "5-6 unrelated test snapshots break" entry), this module pins the +# shared factories to a fixed baseline before every one of its own tests. That makes this file's +# output fully self-determined - independent of suite composition, collection order, or how many +# tests ran before it - so no capture/restore dance and no per-test insulation list are needed +# anymore, here or in any other file in this package. _SHARED_FACTORIES = [ CardFactory, SourceFactory, @@ -43,18 +40,13 @@ CanonicalExpansionFactory, CanonicalCardFactory, ] -_SEQUENCE_INSULATED_TESTS = {"test_only_the_actually_degraded_query_is_flagged_among_several"} @pytest.fixture(autouse=True) -def _preserve_shared_factory_sequences_for_insulated_tests(request): - if request.node.name not in _SEQUENCE_INSULATED_TESTS: - yield - return - before = {f: f._meta.next_sequence() for f in _SHARED_FACTORIES} +def _pin_shared_factory_sequences(): + for f in _SHARED_FACTORIES: + f.reset_sequence(0, force=True) yield - for f, n in before.items(): - f.reset_sequence(n, force=True) def snapshot_response(response: Response, snapshot: SnapshotAssertion): diff --git a/MPCAutofill/cardpicker/tests/test_vote_queue_views.py b/MPCAutofill/cardpicker/tests/test_vote_queue_views.py index 91876caf3..59b670c1f 100644 --- a/MPCAutofill/cardpicker/tests/test_vote_queue_views.py +++ b/MPCAutofill/cardpicker/tests/test_vote_queue_views.py @@ -1,5 +1,3 @@ -import pytest - from django.urls import reverse from cardpicker import views @@ -13,34 +11,13 @@ from cardpicker.tests.factories import ( CanonicalArtistFactory, CanonicalCardFactory, - CanonicalExpansionFactory, CardArtistVoteFactory, CardFactory, CardPrintingTagFactory, CardTagVoteFactory, - SourceFactory, TagFactory, ) -# see test_printing_consensus.py for why this capture-and-restore fixture exists -_SHARED_FACTORIES = [ - CardFactory, - SourceFactory, - CanonicalArtistFactory, - CanonicalExpansionFactory, - CanonicalCardFactory, -] - - -@pytest.fixture(autouse=True) -def _preserve_shared_factory_sequences(): - before = {f: f._meta.next_sequence() for f in _SHARED_FACTORIES} - for f, n in before.items(): - f.reset_sequence(n, force=True) - yield - for f, n in before.items(): - f.reset_sequence(n, force=True) - def _post_vote_queue(client, kind: str, page: int = 1): return client.post( diff --git a/docs/features/catalog-completion-plan.md b/docs/features/catalog-completion-plan.md index ba720b7fb..7d16b42b0 100644 --- a/docs/features/catalog-completion-plan.md +++ b/docs/features/catalog-completion-plan.md @@ -2733,6 +2733,18 @@ likely extends Part 1's existing `PilotRunLedger` rather than a new model. Applies to the shared runner, so Stage C golden runs and the fidelity replay inherit it for free. +Piece (1)'s acceptance test now exists as a re-runnable script, +`scripts/ops/crash_drill.sh` — kill-and-restart at an arbitrary +mid-batch point, a truthful interrupted-run ledger, and a fully +idempotent recovery with zero manual cleanup, exercised against the +live daemon. It first passed in production on 2026-07-23 (kill at +105/1000 committed, `DRILL-PASS 2026-07-23T18:31:39Z`). The script's +one edit-point per run is its seeded cohort (currently a 1,000-card +slice of the blank-collector-text pool, chosen because the +fetch-failed pool the drill originally targeted had collapsed to ~10 +cards, too small to host a reliable kill window) — swap that query +when the current cohort stops being representative or large enough. + --- ## Part 5 — Residual classification (existing tags only) diff --git a/docs/lessons.md b/docs/lessons.md index 6be7a35b1..a58a67b86 100644 --- a/docs/lessons.md +++ b/docs/lessons.md @@ -177,15 +177,24 @@ container. ## `factory.Sequence` counters are process-global for the whole test run Shared factories (`cardpicker/tests/factories.py`) increment a single -sequence counter across every test file in a pytest session, and some -snapshot assertions hardcode exact sequence-derived values (e.g. `"Artist 0"`) that depend on total call count up to that point — so a brand-new, -otherwise-unrelated test file can silently break unrelated snapshots just -by sorting earlier in collection order and using the same factory. Fix -pattern: an autouse fixture local to the new test file(s) only that -captures each shared factory's `next_sequence()` before the test body runs -and calls `reset_sequence(n, force=True)` both immediately (undo the peek's -own increment) and again in teardown, leaving zero net drift. Don't touch -`conftest.py` or existing test files to fix this. +sequence counter across every test file in a pytest session, and a +snapshot assertion that hardcodes a sequence-derived value (e.g. +`"Artist 0"`) depends on total call count up to that point — so a +brand-new, otherwise-unrelated test file could silently break unrelated +snapshots just by sorting earlier in collection order and using the same +factory. Old fix pattern (retired 2026-07-23, see +`docs/troubleshooting.md`'s "5-6 unrelated test snapshots break" entry +for the full history): an autouse fixture local to every _new_ test file +that captured/restored the shared factories around itself, so its own +usage stayed invisible to the rest of the suite — fragile, forgotten +repeatedly across three separate additions. **Current fix**: push the +pin to the one module that actually asserts sequence-derived values +(`test_views.py`) instead of every module that merely uses the shared +factories — an autouse fixture there calls +`Factory.reset_sequence(0, force=True)` on each shared factory before +every one of its own tests, making its snapshots self-determined +regardless of suite composition or collection order. No other test file +needs to protect it anymore. ## Use `du -sh path/.[!.]* path/*`, not a bare `path/*` glob, when sizing what's actually large diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index 8aae22453..8cef52762 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -39,71 +39,56 @@ not just a local re-run — that CI is actually clean afterward. import chain (`update_database → local_phash → imagehash/pytesseract`, `journal/2026-07-16-hash-at-ingest.md`). -## 5-6 unrelated test snapshots break after adding one new test file +## 5-6 unrelated test snapshots break after adding one new test file (RETIRED 2026-07-23) -**Symptom**: a brand-new test file (using an existing shared factory) is -added, and several _other_, seemingly-unrelated tests start failing — -often `test_views.py::TestGetTags::*` or similar snapshot-style +**This whole class of bug is now structurally impossible** — see "Root +fix" below. Kept as history because the root cause (`factory.Sequence` +counters being process-global) is a generically useful fact about +`factory_boy`, and because the old symptom text is still what you'd grep +for if you hit something snapshot-related in this suite. + +**Old symptom**: a brand-new test file (using an existing shared factory) +was added, and several _other_, seemingly-unrelated tests started +failing — often `test_views.py::TestGetTags::*` or similar snapshot-style assertions with a hardcoded value like `"Artist 0"`. **Cause**: `factory.Sequence` counters in `cardpicker/tests/factories.py` -are process-global for the whole pytest run. A snapshot assertion that -hardcodes a sequence-derived value implicitly depends on total call count -up to that point in collection order — a new test file using the same -shared factory shifts that count. - -**Fix**: an autouse fixture local to the new test file(s) only, that -captures each shared factory's `next_sequence()` before the test body -runs and calls `reset_sequence(n, force=True)` both immediately (undo the -peek's own increment) and again in teardown — zero net drift. Don't touch -`conftest.py` or existing test files. - -**Recurred 3 times after being documented** ([[lessons.md]]) because each -new test file has to independently rediscover which factories count as -"shared": deductive-backfill work (had to add `SourceFactory`/ -`CanonicalArtistFactory` to the list), and again in -`test_purge_machine_votes.py` (catalog-completion work). When adding a -test file that uses any factory from `cardpicker/tests/factories.py`, -apply this pattern preemptively rather than waiting to see which -snapshots break. - -**Variant: a new TEST inside an EXISTING file, not a new file** (E-2, -`test_views.py`, `d7e4653c`) — the same drift, different trigger. Adding -one new test to `TestPostEditorSearchResults` (which uses the class's own -function-scoped `populated_database` autouse fixture, same as every other -test in the class) shifted `TestGetSampleCards`/`TestNewCardsFirstPages`/ -`TestNewCardsPage`/`TestPostExploreSearchResults` downstream in the same -file - going from N to N+1 tests using a sequence-consuming fixture -permanently shifts everything after it, file-wide autouse isn't an option -here (it would reset the ambient count for ~200 _other_, correctly-passing -tests in the same file that depend on it). The fix has to be scoped to -just the one new test, which is non-trivial: a same-scope fixture the test -merely _requests_ instantiates too late to see the pre-existing-fixture -"before" value, because same-scope autouse fixtures always instantiate -first. Resolution: a **module-level autouse fixture, gated on -`request.node.name`** - inert (immediate no-op `yield`) for every test -except the one named test, so it wins fixture-ordering priority (module- -level autouse beats class-level autouse at the same nominal scope) while -having zero effect on the rest of the file. See -`test_views.py::_preserve_shared_factory_sequences_for_insulated_tests`. - -**Variant: `--snapshot-update` run against a single file diverges from a -full-suite run** (issue #184's card-payload work). Running -`pytest cardpicker/tests/test_views.py --snapshot-update` in isolation -(to bake in new, purely-additive fields) updated 17 snapshots as expected -— but 6 of those 17 also changed unrelated `Artist N`/etc. sequence-derived -values, because CI (`.github/actions/test-backend/action.yml`) invokes -`pytest .` from `MPCAutofill/`, collecting **every** test file in one -process — several files earlier in collection order consume shared-factory -sequence numbers before `test_views.py` ever runs, and a single-file -invocation skips all of that consumption. **Fix**: always run -`--snapshot-update` against the same scope CI actually uses (`pytest .` -from `MPCAutofill/`, or at minimum every file that shares -`cardpicker/tests/factories.py`'s factories), never a single test file in -isolation — then diff the updated `.ambr` file and confirm it's purely -additive (only the new keys you actually added) before trusting it, since -a same-file-only update can silently bake in wrong sequence-derived values -for tests you didn't otherwise touch. +are process-global for the whole pytest run. `test_views.py` is the only +module in the suite whose assertions embed a sequence-derived value (via +`__snapshots__/test_views.ambr`, reached through +`brainstorm_canonical_card`'s default `CanonicalCardFactory`/ +`CanonicalArtistFactory` SubFactory chain) — so that value implicitly +depended on total call count up to that point in collection order, and +_any_ other file using the same shared factories could shift it. + +**Old fix (retired)**: an autouse fixture local to every _new_ test file +that captured each shared factory's `next_sequence()` before the test +body ran and called `reset_sequence(n, force=True)` both immediately and +again in teardown, keeping that file's own usage invisible to the rest of +the suite. This recurred 3 times after being documented ([[lessons.md]]) +because each new file had to independently rediscover which factories +count as "shared" (deductive-backfill work, then again in +`test_purge_machine_votes.py`), and needed a special-cased, `request.node.name`-gated variant (`test_views.py`'s old +`_preserve_shared_factory_sequences_for_insulated_tests`) for the case of +a single new _test_ inside an existing file, since a same-scope +`populated_database`-consuming test shifted every later test in the same +file. + +**Root fix**: the burden was on the wrong side. Instead of every module +that merely _uses_ the shared factories protecting the one module that +_asserts_ on their exact values, `test_views.py` now pins those factories +to a fixed baseline (`Factory.reset_sequence(0, force=True)`) before every +one of its own tests (`_pin_shared_factory_sequences`, module-level +autouse). Its snapshots are now self-determined regardless of suite +composition, collection order, or how many tests ran before it — no other +file in `cardpicker/tests/` needs to know `test_views.py` exists, and the +old capture/restore fixture + `_SHARED_FACTORIES` list was deleted from +all 31 other files that carried it. This also retired the `--snapshot-update` single-file-vs-full-suite divergence variant that used to +apply here (updating `test_views.ambr` in isolation used to bake in wrong +values because a full-suite run consumed sequence numbers a single-file +run didn't) — since the pin always resets to the same baseline regardless +of what ran before, `pytest cardpicker/tests/test_views.py --snapshot-update` and a full-suite `--snapshot-update` now produce +identical output, both scopes work. ## Seeding rows via a data migration breaks tests that assert a table is empty/complete diff --git a/docs/upstreaming/extractable-primitives.md b/docs/upstreaming/extractable-primitives.md index 1a0ef9915..6d6a1e43c 100644 --- a/docs/upstreaming/extractable-primitives.md +++ b/docs/upstreaming/extractable-primitives.md @@ -108,20 +108,21 @@ coupling to the vote system is. ## Backend -| Primitive | File(s) | Problem solved | Candidate consumers | Entanglement | License note | -| ------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------ | -| Outbound rate limiter ("lh4 rate limiter") | `MPCAutofill/cardpicker/local_phash.py` (`_RateLimiter`, `run_content_phash_backfill`) | Paces a threaded worker pool to a strict `<= N req/sec` ceiling against Google's `lh4.googleusercontent.com` image-resize endpoint | upstream, proxies-at-home (as a copy-paste, not an import — see note) | entangled-with-CanonicalPrinting (colocation) | — | -| Perceptual-hash storage utility | `MPCAutofill/cardpicker/local_phash.py` (`_hash_to_int`, `_int_to_hash`, `compute_card_art_hash`, `find_best_match`) | Encodes/decodes an `imagehash.ImageHash` as a signed 64-bit DB int; threshold+margin best-match selection | upstream, proxies-at-home | entangled-with-CanonicalPrinting (colocation) | — | -| Search-query sanitisation | `MPCAutofill/cardpicker/search/sanitisation.py` | Normalizes free-text queries/names (lowercase, strip bracketed text/punctuation/digits, collapse whitespace) for consistent matching | upstream, proxies-at-home | CLEAN | — | -| Scryfall-style search-operator parser | `MPCAutofill/cardpicker/search/operator_parser.py` (`parse_query`) | Parses a raw query string into residual free text + structured `operator:value`/`-operator:value` tokens (quoted values, case-insensitive operator names, unknown-operator errors) - pure string-in/structure-out, no knowledge of what an operator name maps to downstream | upstream, proxies-at-home | CLEAN | — | -| OCR crop/preprocessing helpers | `MPCAutofill/cardpicker/local_ocr.py` (`crop_collector_line`, `preprocess_variants`, `preprocess_fallback_variants`, `run_tesseract`, `run_tesseract_text_and_words`, `parse_collector_line`, `_normalize_collector_number`, `_median_from_histogram`) | Fractional-bbox crop, grayscale/upscale/threshold-both-polarities preprocessing (plus a heavier-upscale/sharpen + percentile-threshold fallback tier, issue #259) and regex parse of an OCR'd collector-number line | upstream, proxies-at-home | CLEAN | — | -| Image color/quality-signal math | `MPCAutofill/cardpicker/local_image_quality.py` (`is_image_truncated`, `compute_blur_variance`, `compute_entropy`, `compute_color_profile`) | Truncation check, Laplacian-kernel blur variance, grayscale entropy, and per-channel RGB mean/stddev, all pure `PIL.ImageStat`/`ImageFilter` calls against an already-fetched image | upstream, proxies-at-home, federation peers | CLEAN (zero `cardpicker.*` imports at all — only `PIL`) | — | -| Bleed/border geometry helpers | `MPCAutofill/cardpicker/local_fallback.py` (`normalize_crop_box`, `classify_bleed_edge`) | Pure crop-box remapping (bleed vs. trim) and aspect-ratio-based border classification | upstream, proxies-at-home (as a copy-paste, not an import — see note) | entangled-with-vote-consensus (colocation) | — | -| Generic backend utilities | `MPCAutofill/cardpicker/utils.py` (`get_json_endpoint_rate_limited`, `twos_complement`, `section_timer`, `time_to_hours_minutes_seconds`, `log_hours_minutes_seconds_elapsed`) | Rate-limited JSON GET wrapper, signed-int bit-twiddling, timing decorator/formatter | upstream, proxies-at-home | CLEAN | — | -| Batch-flush checkpoint pattern | `MPCAutofill/cardpicker/local_phash.py` (`run_content_phash_backfill`), `deductive_backfill.py` (`run_backfill`), `local_identify_printing_tags.py` (`run_pilot`) | Sliding-window worker pool + periodic bulk-flush + NULL-filter-as-checkpoint for resumable backfill jobs | upstream, proxies-at-home (needs generalizing first — see note) | entangled — no clean instance exists yet | — | -| Elasticsearch connection helpers | `MPCAutofill/cardpicker/search/search_functions.py` (`get_elasticsearch_connection`, `ping_elasticsearch`, `elastic_connection`, `SearchExceptions`) | Thread-local ES client + a decorator translating raw ES connection errors into app exceptions | upstream, proxies-at-home | entangled-with-consensus (colocation) | — | -| Back-face name lookup (issue #199) | `MPCAutofill/cardpicker/printing_metadata_import.py` (`get_back_face_names`, `is_back_face`, `DOUBLE_FACED_LAYOUTS`) | Deterministic name → "is this a known DFC back face" lookup from Scryfall's on-disk `card_faces` bulk data, no network fetch | upstream, proxies-at-home | entangled-with-CanonicalPrinting (colocation) | — | -| Self-recording, forced-dry-run-gated command lifecycle (issue #362) | `MPCAutofill/cardpicker/pilot_run_lifecycle.py` (`resilient_terminal_output`, `enforce_dry_run_precondition`, `add_dry_run_guard_arguments`, `scope_hash`, `initial_counters`, `merge_counters`) | Generic pattern for a long-running write management command: a RUNNING→COMPLETED/FAILED audit-row lifecycle with JSON counters, a broken-pipe-safe terminal-output wrapper, and a forced-dry-run precondition gate refusing `--write`/`--apply` without a matching recent dry-run | upstream, proxies-at-home (any Django project with long-running write management commands) | entangled-with-vote-consensus (colocation) - the one model this file depends on, `PilotRunLedger`, lives in `cardpicker/models.py` alongside the vote system, even though this file itself imports nothing from `vote_consensus`/`printing_consensus`/`tag_consensus`/`artist_consensus`/auth directly | — | +| Primitive | File(s) | Problem solved | Candidate consumers | Entanglement | License note | +| ----------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------ | +| Outbound rate limiter ("lh4 rate limiter") | `MPCAutofill/cardpicker/local_phash.py` (`_RateLimiter`, `run_content_phash_backfill`) | Paces a threaded worker pool to a strict `<= N req/sec` ceiling against Google's `lh4.googleusercontent.com` image-resize endpoint | upstream, proxies-at-home (as a copy-paste, not an import — see note) | entangled-with-CanonicalPrinting (colocation) | — | +| Perceptual-hash storage utility | `MPCAutofill/cardpicker/local_phash.py` (`_hash_to_int`, `_int_to_hash`, `compute_card_art_hash`, `find_best_match`) | Encodes/decodes an `imagehash.ImageHash` as a signed 64-bit DB int; threshold+margin best-match selection | upstream, proxies-at-home | entangled-with-CanonicalPrinting (colocation) | — | +| Search-query sanitisation | `MPCAutofill/cardpicker/search/sanitisation.py` | Normalizes free-text queries/names (lowercase, strip bracketed text/punctuation/digits, collapse whitespace) for consistent matching | upstream, proxies-at-home | CLEAN | — | +| Scryfall-style search-operator parser | `MPCAutofill/cardpicker/search/operator_parser.py` (`parse_query`) | Parses a raw query string into residual free text + structured `operator:value`/`-operator:value` tokens (quoted values, case-insensitive operator names, unknown-operator errors) - pure string-in/structure-out, no knowledge of what an operator name maps to downstream | upstream, proxies-at-home | CLEAN | — | +| OCR crop/preprocessing helpers | `MPCAutofill/cardpicker/local_ocr.py` (`crop_collector_line`, `preprocess_variants`, `preprocess_fallback_variants`, `run_tesseract`, `run_tesseract_text_and_words`, `parse_collector_line`, `_normalize_collector_number`, `_median_from_histogram`) | Fractional-bbox crop, grayscale/upscale/threshold-both-polarities preprocessing (plus a heavier-upscale/sharpen + percentile-threshold fallback tier, issue #259) and regex parse of an OCR'd collector-number line | upstream, proxies-at-home | CLEAN | — | +| Image color/quality-signal math | `MPCAutofill/cardpicker/local_image_quality.py` (`is_image_truncated`, `compute_blur_variance`, `compute_entropy`, `compute_color_profile`) | Truncation check, Laplacian-kernel blur variance, grayscale entropy, and per-channel RGB mean/stddev, all pure `PIL.ImageStat`/`ImageFilter` calls against an already-fetched image | upstream, proxies-at-home, federation peers | CLEAN (zero `cardpicker.*` imports at all — only `PIL`) | — | +| Bleed/border geometry helpers | `MPCAutofill/cardpicker/local_fallback.py` (`normalize_crop_box`, `classify_bleed_edge`) | Pure crop-box remapping (bleed vs. trim) and aspect-ratio-based border classification | upstream, proxies-at-home (as a copy-paste, not an import — see note) | entangled-with-vote-consensus (colocation) | — | +| Generic backend utilities | `MPCAutofill/cardpicker/utils.py` (`get_json_endpoint_rate_limited`, `twos_complement`, `section_timer`, `time_to_hours_minutes_seconds`, `log_hours_minutes_seconds_elapsed`) | Rate-limited JSON GET wrapper, signed-int bit-twiddling, timing decorator/formatter | upstream, proxies-at-home | CLEAN | — | +| Batch-flush checkpoint pattern | `MPCAutofill/cardpicker/local_phash.py` (`run_content_phash_backfill`), `deductive_backfill.py` (`run_backfill`), `local_identify_printing_tags.py` (`run_pilot`) | Sliding-window worker pool + periodic bulk-flush + NULL-filter-as-checkpoint for resumable backfill jobs | upstream, proxies-at-home (needs generalizing first — see note) | entangled — no clean instance exists yet | — | +| Elasticsearch connection helpers | `MPCAutofill/cardpicker/search/search_functions.py` (`get_elasticsearch_connection`, `ping_elasticsearch`, `elastic_connection`, `SearchExceptions`) | Thread-local ES client + a decorator translating raw ES connection errors into app exceptions | upstream, proxies-at-home | entangled-with-consensus (colocation) | — | +| Back-face name lookup (issue #199) | `MPCAutofill/cardpicker/printing_metadata_import.py` (`get_back_face_names`, `is_back_face`, `DOUBLE_FACED_LAYOUTS`) | Deterministic name → "is this a known DFC back face" lookup from Scryfall's on-disk `card_faces` bulk data, no network fetch | upstream, proxies-at-home | entangled-with-CanonicalPrinting (colocation) | — | +| Self-recording, forced-dry-run-gated command lifecycle (issue #362) | `MPCAutofill/cardpicker/pilot_run_lifecycle.py` (`resilient_terminal_output`, `enforce_dry_run_precondition`, `add_dry_run_guard_arguments`, `scope_hash`, `initial_counters`, `merge_counters`) | Generic pattern for a long-running write management command: a RUNNING→COMPLETED/FAILED audit-row lifecycle with JSON counters, a broken-pipe-safe terminal-output wrapper, and a forced-dry-run precondition gate refusing `--write`/`--apply` without a matching recent dry-run | upstream, proxies-at-home (any Django project with long-running write management commands) | entangled-with-vote-consensus (colocation) - the one model this file depends on, `PilotRunLedger`, lives in `cardpicker/models.py` alongside the vote system, even though this file itself imports nothing from `vote_consensus`/`printing_consensus`/`tag_consensus`/`artist_consensus`/auth directly | — | +| Deterministic snapshot-test sequencing (factory-sequence pinning), 2026-07-23 | `MPCAutofill/cardpicker/tests/test_views.py` (`_pin_shared_factory_sequences`) | `factory_boy` `Sequence` counters are process-global for a whole pytest run; a snapshot assertion embedding a sequence-derived value (e.g. an autogenerated `"Artist N"` name) implicitly depends on total call count up to that point in collection order. Rather than every _other_ test module that merely uses the shared factories protecting the one module that asserts on their exact values (the old, repeatedly-forgotten convention — see `docs/troubleshooting.md`'s "5-6 unrelated test snapshots break" entry), the snapshot-owning module pins the shared factories to a fixed baseline (`Factory.reset_sequence(0, force=True)`) before every one of its own tests, making its output self-determined regardless of suite composition, collection order, or how many other tests ran first | upstream, proxies-at-home (any `factory_boy` + snapshot-testing pairing, or any suite with process-global ID/name generators) | CLEAN (pattern-level — the technique itself imports nothing fork-specific; its current call site is `test_views.py`, which does assert some fork-only fields elsewhere in the same file, so this is a pattern to replicate in a fresh file, not a file to lift wholesale — see note) | — | ## Docs tooling & federation @@ -238,6 +239,23 @@ colocation-entangled file" shape as the bleed/border and rate-limiter rows above. Lifting means copying the three names out, not importing the module. +**Deterministic snapshot-test sequencing** — the tether passes this row +narrowly (`test_views.py` imports only `cardpicker.views` and +`cardpicker.tests.constants`/`cardpicker.tests.factories` at module level, +none of which are in `FORK_ONLY_PY_MODULES`, and the tether checks direct +imports one level deep only), but that's a narrower claim than "this file +is generic": `test_views.py` is ~1,500 lines of tests asserting real +fork-only response shape (`printingTagStatus`, `canonicalArtist`, etc.) +throughout. What's actually portable is the ~4-line technique in +`_pin_shared_factory_sequences` itself (call `reset_sequence(0, force=True)` on each factory a module's snapshots transitively depend +on, as a module-level autouse fixture, before every test) — same shape as +the "Eager-WASM lazy-mount fix" frontend row above: a pattern whose call +site happens to live in a larger entangled file, not a file to import. +An extractor wanting this needs `factories.py`'s `Factory.reset_sequence` +usage convention (any `factory_boy` project already has the primitive; +nothing fork-specific to port) plus the four-line fixture body, not +`test_views.py` itself. + ## Not audited this pass `frontend/src/features/clientSearch/` (the Orama-based client-side local/ diff --git a/docs/upstreaming/readiness-audit.md b/docs/upstreaming/readiness-audit.md index 80b657550..eccd546d7 100644 --- a/docs/upstreaming/readiness-audit.md +++ b/docs/upstreaming/readiness-audit.md @@ -123,19 +123,20 @@ same license, same repo tree — not a third-party import). See §9 for the one-time external-origin sweep this ladder itself is exempt from (none of these 11 chunks involve code from outside the fork/upstream tree). -| Chunk | What | LOC | Value | Risk | Depends on | Provenance | -| -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------- | --------------------------------------------------------- | ------------------------- | -------------------------------------------- | ------------------------------------------------------------------------------------------------ | -| **A. Printing-tag vote system (core)** | Weighted-consensus engine (`AbstractWeightedVote`, `VoteSource`) + `CanonicalPrintingMetadata`/`CardPrintingTag`, printing-candidate search, ES re-rank hook | ~700 (migrations `0050`–`0051`) | **(a)** | Low | none — foundation | own-work | -| **B. Artist + tag weighted voting, unified queue** | Extends A to artist/tag attributes, `2/voteQueue/` | ~1,300 (migrations `0052`–`0055`, `0057`) | **(a)** | Medium | A | own-work | -| **C. Tag taxonomy (Stage 3)** | Seeded `Tag` rows + fuzzy filename matching, `display_name` split | ~500 (migration `0056`) | **(b)** | Low | none (functional dep on B) | upstream-GPL3 (extends upstream's existing `Tag` model) | -| **D. Unified question feed** | `2/questionFeed/`, single prioritized review stream replacing the 3-tab switcher | ~600 | **(a)** | Medium | A, B, F | own-work | -| **E. Moderation layer** | Discord OAuth, privileged-vote co-sign gate, `CardReport`, Reports/Drives admin queue | ~1,300 (migrations `0057`–`0059`) | **(b)**, mechanism only | Medium-high | A, B, C | own-work | -| **F. Deductive printing-tag backfill** | Logic-only vote casting (D1/D2 tiers) for logically-entailed printings | ~600 | **(a)** | Low | A, C | own-work | -| **G. Local OCR/phash identification pilot** | Tesseract + perceptual-hash + fallback engines, run-cohort revocability | **~6,700** (migrations `0060`–`0064`) | **(a) with caveats** — pilot-status, fork-tuned constants | **High** | A, F, I | own-work (uses the real `pytesseract`/`imagehash` PyPI deps, not copied algorithm code — see §9) | -| **H. `LOCAL_FILE` source type** | Real implementation of upstream's already-anticipated stub; local-disk cataloging + path-traversal-safe image serving | ~320 | **(a)** | **Low** | none — most self-contained chunk in the diff | upstream-GPL3 (completes a stub upstream's own schema/enum already defined) | -| **I. Image-CDN backend fetch helper** | `image_cdn_fetch.py`, server-side full-res fetch for OCR/hashing | ~77 | (c) as written | Medium | Worker/R2 infra (out of scope) | own-work | -| **J. Decklist printing-aware line formatting** | `format_decklist_line()` appends `(SET) NUM` when the import API gave printing data | ~50 (entangled in a 107-line file diff) | **(a)** | Low, but needs hunk-split | none | upstream-GPL3 (edits an existing upstream file) | -| **K. Ours-only operational settings** | Sentry removal, CORS branding, Discord/session-cookie env wiring | interleaved in `settings.py` | (c) | N/A — strip, don't port | — | upstream-GPL3 (edits upstream's existing `settings.py`) | +| Chunk | What | LOC | Value | Risk | Depends on | Provenance | +| --------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- | ------------------------- | -------------------------------------------- | ------------------------------------------------------------------------------------------------ | +| **A. Printing-tag vote system (core)** | Weighted-consensus engine (`AbstractWeightedVote`, `VoteSource`) + `CanonicalPrintingMetadata`/`CardPrintingTag`, printing-candidate search, ES re-rank hook | ~700 (migrations `0050`–`0051`) | **(a)** | Low | none — foundation | own-work | +| **B. Artist + tag weighted voting, unified queue** | Extends A to artist/tag attributes, `2/voteQueue/` | ~1,300 (migrations `0052`–`0055`, `0057`) | **(a)** | Medium | A | own-work | +| **C. Tag taxonomy (Stage 3)** | Seeded `Tag` rows + fuzzy filename matching, `display_name` split | ~500 (migration `0056`) | **(b)** | Low | none (functional dep on B) | upstream-GPL3 (extends upstream's existing `Tag` model) | +| **D. Unified question feed** | `2/questionFeed/`, single prioritized review stream replacing the 3-tab switcher | ~600 | **(a)** | Medium | A, B, F | own-work | +| **E. Moderation layer** | Discord OAuth, privileged-vote co-sign gate, `CardReport`, Reports/Drives admin queue | ~1,300 (migrations `0057`–`0059`) | **(b)**, mechanism only | Medium-high | A, B, C | own-work | +| **F. Deductive printing-tag backfill** | Logic-only vote casting (D1/D2 tiers) for logically-entailed printings | ~600 | **(a)** | Low | A, C | own-work | +| **G. Local OCR/phash identification pilot** | Tesseract + perceptual-hash + fallback engines, run-cohort revocability | **~6,700** (migrations `0060`–`0064`) | **(a) with caveats** — pilot-status, fork-tuned constants | **High** | A, F, I | own-work (uses the real `pytesseract`/`imagehash` PyPI deps, not copied algorithm code — see §9) | +| **H. `LOCAL_FILE` source type** | Real implementation of upstream's already-anticipated stub; local-disk cataloging + path-traversal-safe image serving | ~320 | **(a)** | **Low** | none — most self-contained chunk in the diff | upstream-GPL3 (completes a stub upstream's own schema/enum already defined) | +| **I. Image-CDN backend fetch helper** | `image_cdn_fetch.py`, server-side full-res fetch for OCR/hashing | ~77 | (c) as written | Medium | Worker/R2 infra (out of scope) | own-work | +| **J. Decklist printing-aware line formatting** | `format_decklist_line()` appends `(SET) NUM` when the import API gave printing data | ~50 (entangled in a 107-line file diff) | **(a)** | Low, but needs hunk-split | none | upstream-GPL3 (edits an existing upstream file) | +| **K. Ours-only operational settings** | Sentry removal, CORS branding, Discord/session-cookie env wiring | interleaved in `settings.py` | (c) | N/A — strip, don't port | — | upstream-GPL3 (edits upstream's existing `settings.py`) | +| **L. Backend test-infra: deterministic snapshot-sequencing pattern** (2026-07-23) | Pins `factory_boy` `Sequence` counters to a fixed baseline (`reset_sequence(0, force=True)`) per test in the one snapshot-asserting test module (`test_views.py`), instead of the old convention where every _other_ test file using a shared factory had to capture/restore its own use of it around itself so the snapshot module's hardcoded values stayed put — see `docs/upstreaming/extractable-primitives.md`'s Backend table for the full write-up | ~700 removed / ~15 added (net -685), test-only, no runtime diff | **(a)** as a pattern (test-infra, not a shippable file — see extractable-primitives.md's detail note) | **Very low** | none | own-work | Full per-chunk detail (files, tests, exact dependency chains, entanglement notes) is in the backend research pass; the table above is @@ -298,6 +299,10 @@ review as one PR each — these are what Phase 2 should draw from. in `image-cdn/`, not `frontend/`, correcting the original brief) — self-contained Worker module + 6 tests, fixes an accidental (not intentional) CORS pass-through as a side effect. +12. **Backend test-infra: deterministic snapshot-sequencing pattern** + (backend L) — a four-line `factory_boy` fixture technique, not a file; + zero fork-only imports, zero deps, applicable to any project pairing + `factory_boy` with snapshot testing. ### Tier 2 — real value, moderate prep diff --git a/scripts/ops/crash_drill.sh b/scripts/ops/crash_drill.sh new file mode 100755 index 000000000..a6bd37965 --- /dev/null +++ b/scripts/ops/crash_drill.sh @@ -0,0 +1,94 @@ +#!/usr/bin/env bash +# crash_drill.sh v2 (post-review) — kill-safety acceptance test over the +# fetch-failed cohort. HONEST SCOPE (review finding): the --card-ids-file +# path deliberately bypasses the bulk resume-skip filter, so this drill +# proves: (a) a kill -9 mid-batch loses no committed work and corrupts +# nothing; (b) the interrupted run's ledger tells the truth; (c) an +# identical re-invocation completes idempotently with zero manual cleanup. +# The bulk-path resume-skip filter itself is unit-tested; its live proof +# rides the next natural --limit harvest. Doubles as the fetch-failed +# retry pass (#381). Wall time worst case ~1.5h (timeout-bound fetches). +# NOTE for the executor/owner: a GoogleFetchLockoutError failure means the +# environment locked us out mid-drill, not that resume logic is broken. +set -euo pipefail +LOGDIR="$HOME/.local/share/proxyprints-daemon/drills"; mkdir -p "$LOGDIR" +LOG="$LOGDIR/drill-$(date -u +%Y%m%dT%H%M%SZ).log"; exec > >(tee -a "$LOG") 2>&1 +TS=$(date -u +%Y%m%dT%H%MZ); RUN="crash-drill-$TS"; IDS=/MPCAutofill/MPCAutofill/drill_fetchfail_ids.txt +DEX="docker exec -w /MPCAutofill/MPCAutofill mpcautofill_django" +echo "drill start $(date -u +%FT%TZ) run_id=$RUN" + +# v4 cohort: the fetch-failed pool collapsed to ~10 after the 2026-07-23 retry +# pass, too small to host a kill window. Use a 1,000-card seeded slice of the +# blank-collector-text pool instead: re-extraction there is idempotent evidence +# refresh (harmless, useful), and ~4 minutes of run gives the window room. +$DEX python manage.py shell -c " +from cardpicker.models import ImageEvidence +import random +pool = sorted(set(ImageEvidence.objects.filter(fetch_ok=True, collector_line_raw_text='').exclude(run_id='ntx-0721').values_list('card_id', flat=True))) +ids = sorted(random.Random(20260723).sample(pool, min(1000, len(pool)))) +open('$IDS','w').write('\n'.join(map(str, ids))) +print('cohort:', len(ids), 'of pool', len(pool))" + +echo "phase 2: dry-run (guard satisfaction, same scope)" +$DEX python manage.py run_image_evidence_cohort --card-ids-file "$IDS" --dry-run --run-id "$RUN-dry" + +echo "phase 3: write + kill -9 mid-batch" +$DEX python manage.py run_image_evidence_cohort --card-ids-file "$IDS" --run-id "$RUN-w1" & +CLIENT=$! +# Progress-triggered kill: strike only while verifiably mid-batch (5%-85% +# committed), sampling every 15s. Avoids spurious too-early/too-late failures +# from fixed timers on a cohort whose per-card time varies 100x (fast 404 vs +# 15s timeout). If the run finishes before reaching the window, that is a +# fail-closed exit 3 (nothing was provable), not a contract failure. +TOTAL=$(grep -c . <($DEX cat "$IDS") 2>/dev/null || $DEX python -c "print(sum(1 for l in open('$IDS') if l.strip()))") +LOW=$(( TOTAL / 20 )); [ "$LOW" -lt 1 ] && LOW=1; HIGH=$(( TOTAL * 85 / 100 )) +KILLED=0 +for i in $(seq 1 120); do + sleep 15 + if ! kill -0 $CLIENT 2>/dev/null; then break; fi + DONE=$($DEX python manage.py shell -c "from cardpicker.models import ImageEvidence; print(ImageEvidence.objects.filter(run_id='$RUN-w1').values('card_id').distinct().count())" 2>/dev/null | tail -1) + echo " progress sample: $DONE / $TOTAL committed" + case "$DONE" in (*[!0-9]*|"") continue;; esac + if [ "$DONE" -ge "$LOW" ] && [ "$DONE" -le "$HIGH" ]; then + # v5: the container image has neither pgrep nor kill (nor ps) — discover + # and signal from the HOST: docker top prints host-namespace PIDs. + PID=$(sudo docker top mpcautofill_django | grep run_image_evidence_cohort | grep -v grep | awk '{print $2}' | head -1 || true) + case "$PID" in (*[!0-9]*|"") continue;; esac + echo "killing host pid $PID (container run) at $DONE/$TOTAL committed, $(date -u +%FT%TZ)" + sudo kill -9 "$PID" + KILLED=1 + break + fi +done +[ "$KILLED" != "1" ] && { echo "DRILL-FAIL: never caught the run mid-window (finished too fast, stalled pre-window, or never started) - nothing provable this pass"; kill $CLIENT 2>/dev/null || true; exit 3; } +wait $CLIENT || echo "client exited nonzero after kill (expected)" + +echo "phase 4: interrupted-state truth + durable partial work" +$DEX python manage.py shell -c " +from cardpicker.models import PilotRunLedger, ImageEvidence +r = PilotRunLedger.objects.filter(run_id='$RUN-w1').first() +assert r is not None, 'no ledger row for interrupted run' +assert r.status != 'completed', 'w1 marked completed despite kill -9: status lies' +ids = [int(x) for x in open('$IDS') if x.strip()] +w1_done = ImageEvidence.objects.filter(card_id__in=ids, run_id='$RUN-w1').values('card_id').distinct().count() +print('w1 ledger status:', r.status, '| w1 committed cards:', w1_done, 'of', len(ids)) +assert w1_done > 0, 'kill landed before ANY committed work - drill window too early to prove durability' +assert w1_done < len(ids), 'w1 finished everything before the kill - drill window too late' +open('/tmp/drill_w1_done.txt','w').write(str(w1_done))" + +echo "phase 5: identical re-invocation (zero manual cleanup; override logged by design)" +$DEX python manage.py run_image_evidence_cohort --card-ids-file "$IDS" --skip-dryrun-check --run-id "$RUN-w2" + +echo "phase 6: final-state verification" +$DEX python manage.py shell -c " +from cardpicker.models import PilotRunLedger, ImageEvidence +w2 = PilotRunLedger.objects.filter(run_id='$RUN-w2').first() +assert w2 is not None and w2.status == 'completed', 'retry run did not complete: %s' % (w2.status if w2 else None) +ids = [int(x) for x in open('$IDS') if x.strip()] +covered = ImageEvidence.objects.filter(card_id__in=ids).values('card_id').distinct().count() +assert covered == len(ids), 'evidence coverage incomplete after retry: %d of %d' % (covered, len(ids)) +w1_done = int(open('/tmp/drill_w1_done.txt').read()) +now_ok = ImageEvidence.objects.filter(card_id__in=ids, fetch_ok=True).values('card_id').distinct().count() +print('retry completed | cohort', len(ids), '| w1 partial survived:', w1_done, '| now fetch_ok:', now_ok)" +docker exec mpcautofill_django rm -f "$IDS" || true +echo "DRILL-PASS $(date -u +%FT%TZ) log=$LOG" From 7911f3a920084bd7d347a388bca994d9c458786b Mon Sep 17 00:00:00 2001 From: wilfordgrimley <2397930+WilfordGrimley@users.noreply.github.com> Date: Fri, 24 Jul 2026 15:36:57 +0000 Subject: [PATCH 2/2] Fix committed conflict markers in two docs files --- docs/features/catalog-completion-plan.md | 5 +-- docs/upstreaming/extractable-primitives.md | 48 +++++++--------------- 2 files changed, 16 insertions(+), 37 deletions(-) diff --git a/docs/features/catalog-completion-plan.md b/docs/features/catalog-completion-plan.md index 0ba218567..8f4fb8dad 100644 --- a/docs/features/catalog-completion-plan.md +++ b/docs/features/catalog-completion-plan.md @@ -2805,7 +2805,6 @@ likely extends Part 1's existing `PilotRunLedger` rather than a new model. Applies to the shared runner, so Stage C golden runs and the fidelity replay inherit it for free. -<<<<<<< HEAD Piece (1)'s acceptance test now exists as a re-runnable script, `scripts/ops/crash_drill.sh` — kill-and-restart at an arbitrary mid-batch point, a truthful interrupted-run ledger, and a fully @@ -2817,7 +2816,7 @@ slice of the blank-collector-text pool, chosen because the fetch-failed pool the drill originally targeted had collapsed to ~10 cards, too small to host a reliable kill window) — swap that query when the current cohort stops being representative or large enough. -======= + The batch-run contract above is fully specified and its kill-test has passed in production (see the crash-drill note earlier in this section); extending the same four-piece contract to a continuous @@ -2827,8 +2826,6 @@ world, observability) is designed in [`docs/proposals/stage-e-streaming.md`](../proposals/stage-e-streaming.md) (issue #153, HOLD pending owner review) — not restated here. -> > > > > > > 3a342e6964a1e54fd8bd6a5e9d0ecb2975687e61 - --- ## Part 5 — Residual classification (existing tags only) diff --git a/docs/upstreaming/extractable-primitives.md b/docs/upstreaming/extractable-primitives.md index a3b23f867..9f651694a 100644 --- a/docs/upstreaming/extractable-primitives.md +++ b/docs/upstreaming/extractable-primitives.md @@ -109,39 +109,21 @@ coupling to the vote system is. ## Backend -<<<<<<< HEAD -| Primitive | File(s) | Problem solved | Candidate consumers | Entanglement | License note | -| ----------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------ | -| Outbound rate limiter ("lh4 rate limiter") | `MPCAutofill/cardpicker/local_phash.py` (`_RateLimiter`, `run_content_phash_backfill`) | Paces a threaded worker pool to a strict `<= N req/sec` ceiling against Google's `lh4.googleusercontent.com` image-resize endpoint | upstream, proxies-at-home (as a copy-paste, not an import — see note) | entangled-with-CanonicalPrinting (colocation) | — | -| Perceptual-hash storage utility | `MPCAutofill/cardpicker/local_phash.py` (`_hash_to_int`, `_int_to_hash`, `compute_card_art_hash`, `find_best_match`) | Encodes/decodes an `imagehash.ImageHash` as a signed 64-bit DB int; threshold+margin best-match selection | upstream, proxies-at-home | entangled-with-CanonicalPrinting (colocation) | — | -| Search-query sanitisation | `MPCAutofill/cardpicker/search/sanitisation.py` | Normalizes free-text queries/names (lowercase, strip bracketed text/punctuation/digits, collapse whitespace) for consistent matching | upstream, proxies-at-home | CLEAN | — | -| Scryfall-style search-operator parser | `MPCAutofill/cardpicker/search/operator_parser.py` (`parse_query`) | Parses a raw query string into residual free text + structured `operator:value`/`-operator:value` tokens (quoted values, case-insensitive operator names, unknown-operator errors) - pure string-in/structure-out, no knowledge of what an operator name maps to downstream | upstream, proxies-at-home | CLEAN | — | -| OCR crop/preprocessing helpers | `MPCAutofill/cardpicker/local_ocr.py` (`crop_collector_line`, `preprocess_variants`, `preprocess_fallback_variants`, `run_tesseract`, `run_tesseract_text_and_words`, `parse_collector_line`, `_normalize_collector_number`, `_median_from_histogram`) | Fractional-bbox crop, grayscale/upscale/threshold-both-polarities preprocessing (plus a heavier-upscale/sharpen + percentile-threshold fallback tier, issue #259) and regex parse of an OCR'd collector-number line | upstream, proxies-at-home | CLEAN | — | -| Image color/quality-signal math | `MPCAutofill/cardpicker/local_image_quality.py` (`is_image_truncated`, `compute_blur_variance`, `compute_entropy`, `compute_color_profile`) | Truncation check, Laplacian-kernel blur variance, grayscale entropy, and per-channel RGB mean/stddev, all pure `PIL.ImageStat`/`ImageFilter` calls against an already-fetched image | upstream, proxies-at-home, federation peers | CLEAN (zero `cardpicker.*` imports at all — only `PIL`) | — | -| Bleed/border geometry helpers | `MPCAutofill/cardpicker/local_fallback.py` (`normalize_crop_box`, `classify_bleed_edge`) | Pure crop-box remapping (bleed vs. trim) and aspect-ratio-based border classification | upstream, proxies-at-home (as a copy-paste, not an import — see note) | entangled-with-vote-consensus (colocation) | — | -| Generic backend utilities | `MPCAutofill/cardpicker/utils.py` (`get_json_endpoint_rate_limited`, `twos_complement`, `section_timer`, `time_to_hours_minutes_seconds`, `log_hours_minutes_seconds_elapsed`) | Rate-limited JSON GET wrapper, signed-int bit-twiddling, timing decorator/formatter | upstream, proxies-at-home | CLEAN | — | -| Batch-flush checkpoint pattern | `MPCAutofill/cardpicker/local_phash.py` (`run_content_phash_backfill`), `deductive_backfill.py` (`run_backfill`), `local_identify_printing_tags.py` (`run_pilot`) | Sliding-window worker pool + periodic bulk-flush + NULL-filter-as-checkpoint for resumable backfill jobs | upstream, proxies-at-home (needs generalizing first — see note) | entangled — no clean instance exists yet | — | -| Elasticsearch connection helpers | `MPCAutofill/cardpicker/search/search_functions.py` (`get_elasticsearch_connection`, `ping_elasticsearch`, `elastic_connection`, `SearchExceptions`) | Thread-local ES client + a decorator translating raw ES connection errors into app exceptions | upstream, proxies-at-home | entangled-with-consensus (colocation) | — | -| Back-face name lookup (issue #199) | `MPCAutofill/cardpicker/printing_metadata_import.py` (`get_back_face_names`, `is_back_face`, `DOUBLE_FACED_LAYOUTS`) | Deterministic name → "is this a known DFC back face" lookup from Scryfall's on-disk `card_faces` bulk data, no network fetch | upstream, proxies-at-home | entangled-with-CanonicalPrinting (colocation) | — | -| Self-recording, forced-dry-run-gated command lifecycle (issue #362) | `MPCAutofill/cardpicker/pilot_run_lifecycle.py` (`resilient_terminal_output`, `enforce_dry_run_precondition`, `add_dry_run_guard_arguments`, `scope_hash`, `initial_counters`, `merge_counters`) | Generic pattern for a long-running write management command: a RUNNING→COMPLETED/FAILED audit-row lifecycle with JSON counters, a broken-pipe-safe terminal-output wrapper, and a forced-dry-run precondition gate refusing `--write`/`--apply` without a matching recent dry-run | upstream, proxies-at-home (any Django project with long-running write management commands) | entangled-with-vote-consensus (colocation) - the one model this file depends on, `PilotRunLedger`, lives in `cardpicker/models.py` alongside the vote system, even though this file itself imports nothing from `vote_consensus`/`printing_consensus`/`tag_consensus`/`artist_consensus`/auth directly | — | -| Deterministic snapshot-test sequencing (factory-sequence pinning), 2026-07-23 | `MPCAutofill/cardpicker/tests/test_views.py` (`_pin_shared_factory_sequences`) | `factory_boy` `Sequence` counters are process-global for a whole pytest run; a snapshot assertion embedding a sequence-derived value (e.g. an autogenerated `"Artist N"` name) implicitly depends on total call count up to that point in collection order. Rather than every _other_ test module that merely uses the shared factories protecting the one module that asserts on their exact values (the old, repeatedly-forgotten convention — see `docs/troubleshooting.md`'s "5-6 unrelated test snapshots break" entry), the snapshot-owning module pins the shared factories to a fixed baseline (`Factory.reset_sequence(0, force=True)`) before every one of its own tests, making its output self-determined regardless of suite composition, collection order, or how many other tests ran first | upstream, proxies-at-home (any `factory_boy` + snapshot-testing pairing, or any suite with process-global ID/name generators) | CLEAN (pattern-level — the technique itself imports nothing fork-specific; its current call site is `test_views.py`, which does assert some fork-only fields elsewhere in the same file, so this is a pattern to replicate in a fresh file, not a file to lift wholesale — see note) | — | -======= -| Primitive | File(s) | Problem solved | Candidate consumers | Entanglement | License note | -| ------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------ | -| Outbound rate limiter ("lh4 rate limiter") | `MPCAutofill/cardpicker/local_phash.py` (`_RateLimiter`, `run_content_phash_backfill`) | Paces a threaded worker pool to a strict `<= N req/sec` ceiling against Google's `lh4.googleusercontent.com` image-resize endpoint | upstream, proxies-at-home (as a copy-paste, not an import — see note) | entangled-with-CanonicalPrinting (colocation) | — | -| Perceptual-hash storage utility | `MPCAutofill/cardpicker/local_phash.py` (`_hash_to_int`, `_int_to_hash`, `compute_card_art_hash`, `find_best_match`) | Encodes/decodes an `imagehash.ImageHash` as a signed 64-bit DB int; threshold+margin best-match selection | upstream, proxies-at-home | entangled-with-CanonicalPrinting (colocation) | — | -| Search-query sanitisation | `MPCAutofill/cardpicker/search/sanitisation.py` | Normalizes free-text queries/names (lowercase, strip bracketed text/punctuation/digits, collapse whitespace) for consistent matching | upstream, proxies-at-home | CLEAN | — | -| Scryfall-style search-operator parser | `MPCAutofill/cardpicker/search/operator_parser.py` (`parse_query`) | Parses a raw query string into residual free text + structured `operator:value`/`-operator:value` tokens (quoted values, case-insensitive operator names, unknown-operator errors) - pure string-in/structure-out, no knowledge of what an operator name maps to downstream | upstream, proxies-at-home | CLEAN | — | -| OCR crop/preprocessing helpers | `MPCAutofill/cardpicker/local_ocr.py` (`crop_collector_line`, `preprocess_variants`, `preprocess_fallback_variants`, `run_tesseract`, `run_tesseract_text_and_words`, `parse_collector_line`, `parse_legal_line`, `_normalize_collector_number`, `_median_from_histogram`) | Fractional-bbox crop, grayscale/upscale/threshold-both-polarities preprocessing (plus a heavier-upscale/sharpen + percentile-threshold fallback tier, issue #259), regex parse of an OCR'd collector-number line, and regex-based "not for sale"/proxy-marker/playtest/copyright-year detection over an OCR'd legal line (2026-07-23, PR #384's marker-detection expansion - widened to catch maker-brand-glued forms like "JestaProxy") | upstream, proxies-at-home | CLEAN | — | -| Image color/quality-signal math | `MPCAutofill/cardpicker/local_image_quality.py` (`is_image_truncated`, `compute_blur_variance`, `compute_entropy`, `compute_color_profile`) | Truncation check, Laplacian-kernel blur variance, grayscale entropy, and per-channel RGB mean/stddev, all pure `PIL.ImageStat`/`ImageFilter` calls against an already-fetched image | upstream, proxies-at-home, federation peers | CLEAN (zero `cardpicker.*` imports at all — only `PIL`) | — | -| Bleed/border geometry helpers | `MPCAutofill/cardpicker/local_fallback.py` (`normalize_crop_box`, `classify_bleed_edge`) | Pure crop-box remapping (bleed vs. trim) and aspect-ratio-based border classification | upstream, proxies-at-home (as a copy-paste, not an import — see note) | entangled-with-vote-consensus (colocation) | — | -| Generic backend utilities | `MPCAutofill/cardpicker/utils.py` (`get_json_endpoint_rate_limited`, `twos_complement`, `section_timer`, `time_to_hours_minutes_seconds`, `log_hours_minutes_seconds_elapsed`) | Rate-limited JSON GET wrapper, signed-int bit-twiddling, timing decorator/formatter | upstream, proxies-at-home | CLEAN | — | -| Batch-flush checkpoint pattern | `MPCAutofill/cardpicker/local_phash.py` (`run_content_phash_backfill`), `deductive_backfill.py` (`run_backfill`), `local_identify_printing_tags.py` (`run_pilot`) | Sliding-window worker pool + periodic bulk-flush + NULL-filter-as-checkpoint for resumable backfill jobs | upstream, proxies-at-home (needs generalizing first — see note) | entangled — no clean instance exists yet | — | -| Elasticsearch connection helpers | `MPCAutofill/cardpicker/search/search_functions.py` (`get_elasticsearch_connection`, `ping_elasticsearch`, `elastic_connection`, `SearchExceptions`) | Thread-local ES client + a decorator translating raw ES connection errors into app exceptions | upstream, proxies-at-home | entangled-with-consensus (colocation) | — | -| Back-face name lookup (issue #199) | `MPCAutofill/cardpicker/printing_metadata_import.py` (`get_back_face_names`, `is_back_face`, `DOUBLE_FACED_LAYOUTS`) | Deterministic name → "is this a known DFC back face" lookup from Scryfall's on-disk `card_faces` bulk data, no network fetch | upstream, proxies-at-home | entangled-with-CanonicalPrinting (colocation) | — | -| Self-recording, forced-dry-run-gated command lifecycle (issue #362) | `MPCAutofill/cardpicker/pilot_run_lifecycle.py` (`resilient_terminal_output`, `enforce_dry_run_precondition`, `add_dry_run_guard_arguments`, `scope_hash`, `initial_counters`, `merge_counters`) | Generic pattern for a long-running write management command: a RUNNING→COMPLETED/FAILED audit-row lifecycle with JSON counters, a broken-pipe-safe terminal-output wrapper, and a forced-dry-run precondition gate refusing `--write`/`--apply` without a matching recent dry-run | upstream, proxies-at-home (any Django project with long-running write management commands) | entangled-with-vote-consensus (colocation) - the one model this file depends on, `PilotRunLedger`, lives in `cardpicker/models.py` alongside the vote system, even though this file itself imports nothing from `vote_consensus`/`printing_consensus`/`tag_consensus`/`artist_consensus`/auth directly | — | - -> > > > > > > 3a342e6964a1e54fd8bd6a5e9d0ecb2975687e61 +| Primitive | File(s) | Problem solved | Candidate consumers | Entanglement | License note | +| ----------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------ | +| Outbound rate limiter ("lh4 rate limiter") | `MPCAutofill/cardpicker/local_phash.py` (`_RateLimiter`, `run_content_phash_backfill`) | Paces a threaded worker pool to a strict `<= N req/sec` ceiling against Google's `lh4.googleusercontent.com` image-resize endpoint | upstream, proxies-at-home (as a copy-paste, not an import — see note) | entangled-with-CanonicalPrinting (colocation) | — | +| Perceptual-hash storage utility | `MPCAutofill/cardpicker/local_phash.py` (`_hash_to_int`, `_int_to_hash`, `compute_card_art_hash`, `find_best_match`) | Encodes/decodes an `imagehash.ImageHash` as a signed 64-bit DB int; threshold+margin best-match selection | upstream, proxies-at-home | entangled-with-CanonicalPrinting (colocation) | — | +| Search-query sanitisation | `MPCAutofill/cardpicker/search/sanitisation.py` | Normalizes free-text queries/names (lowercase, strip bracketed text/punctuation/digits, collapse whitespace) for consistent matching | upstream, proxies-at-home | CLEAN | — | +| Scryfall-style search-operator parser | `MPCAutofill/cardpicker/search/operator_parser.py` (`parse_query`) | Parses a raw query string into residual free text + structured `operator:value`/`-operator:value` tokens (quoted values, case-insensitive operator names, unknown-operator errors) - pure string-in/structure-out, no knowledge of what an operator name maps to downstream | upstream, proxies-at-home | CLEAN | — | +| OCR crop/preprocessing helpers | `MPCAutofill/cardpicker/local_ocr.py` (`crop_collector_line`, `preprocess_variants`, `preprocess_fallback_variants`, `run_tesseract`, `run_tesseract_text_and_words`, `parse_collector_line`, `parse_legal_line`, `_normalize_collector_number`, `_median_from_histogram`) | Fractional-bbox crop, grayscale/upscale/threshold-both-polarities preprocessing (plus a heavier-upscale/sharpen + percentile-threshold fallback tier, issue #259), regex parse of an OCR'd collector-number line, and regex-based "not for sale"/proxy-marker/playtest/copyright-year detection over an OCR'd legal line (2026-07-23, PR #384's marker-detection expansion - widened to catch maker-brand-glued forms like "JestaProxy") | upstream, proxies-at-home | CLEAN | — | +| Image color/quality-signal math | `MPCAutofill/cardpicker/local_image_quality.py` (`is_image_truncated`, `compute_blur_variance`, `compute_entropy`, `compute_color_profile`) | Truncation check, Laplacian-kernel blur variance, grayscale entropy, and per-channel RGB mean/stddev, all pure `PIL.ImageStat`/`ImageFilter` calls against an already-fetched image | upstream, proxies-at-home, federation peers | CLEAN (zero `cardpicker.*` imports at all — only `PIL`) | — | +| Bleed/border geometry helpers | `MPCAutofill/cardpicker/local_fallback.py` (`normalize_crop_box`, `classify_bleed_edge`) | Pure crop-box remapping (bleed vs. trim) and aspect-ratio-based border classification | upstream, proxies-at-home (as a copy-paste, not an import — see note) | entangled-with-vote-consensus (colocation) | — | +| Generic backend utilities | `MPCAutofill/cardpicker/utils.py` (`get_json_endpoint_rate_limited`, `twos_complement`, `section_timer`, `time_to_hours_minutes_seconds`, `log_hours_minutes_seconds_elapsed`) | Rate-limited JSON GET wrapper, signed-int bit-twiddling, timing decorator/formatter | upstream, proxies-at-home | CLEAN | — | +| Batch-flush checkpoint pattern | `MPCAutofill/cardpicker/local_phash.py` (`run_content_phash_backfill`), `deductive_backfill.py` (`run_backfill`), `local_identify_printing_tags.py` (`run_pilot`) | Sliding-window worker pool + periodic bulk-flush + NULL-filter-as-checkpoint for resumable backfill jobs | upstream, proxies-at-home (needs generalizing first — see note) | entangled — no clean instance exists yet | — | +| Elasticsearch connection helpers | `MPCAutofill/cardpicker/search/search_functions.py` (`get_elasticsearch_connection`, `ping_elasticsearch`, `elastic_connection`, `SearchExceptions`) | Thread-local ES client + a decorator translating raw ES connection errors into app exceptions | upstream, proxies-at-home | entangled-with-consensus (colocation) | — | +| Back-face name lookup (issue #199) | `MPCAutofill/cardpicker/printing_metadata_import.py` (`get_back_face_names`, `is_back_face`, `DOUBLE_FACED_LAYOUTS`) | Deterministic name → "is this a known DFC back face" lookup from Scryfall's on-disk `card_faces` bulk data, no network fetch | upstream, proxies-at-home | entangled-with-CanonicalPrinting (colocation) | — | +| Self-recording, forced-dry-run-gated command lifecycle (issue #362) | `MPCAutofill/cardpicker/pilot_run_lifecycle.py` (`resilient_terminal_output`, `enforce_dry_run_precondition`, `add_dry_run_guard_arguments`, `scope_hash`, `initial_counters`, `merge_counters`) | Generic pattern for a long-running write management command: a RUNNING→COMPLETED/FAILED audit-row lifecycle with JSON counters, a broken-pipe-safe terminal-output wrapper, and a forced-dry-run precondition gate refusing `--write`/`--apply` without a matching recent dry-run | upstream, proxies-at-home (any Django project with long-running write management commands) | entangled-with-vote-consensus (colocation) - the one model this file depends on, `PilotRunLedger`, lives in `cardpicker/models.py` alongside the vote system, even though this file itself imports nothing from `vote_consensus`/`printing_consensus`/`tag_consensus`/`artist_consensus`/auth directly | — | +| Deterministic snapshot-test sequencing (factory-sequence pinning), 2026-07-23 | `MPCAutofill/cardpicker/tests/test_views.py` (`_pin_shared_factory_sequences`) | `factory_boy` `Sequence` counters are process-global for a whole pytest run; a snapshot assertion embedding a sequence-derived value (e.g. an autogenerated `"Artist N"` name) implicitly depends on total call count up to that point in collection order. Rather than every _other_ test module that merely uses the shared factories protecting the one module that asserts on their exact values (the old, repeatedly-forgotten convention — see `docs/troubleshooting.md`'s "5-6 unrelated test snapshots break" entry), the snapshot-owning module pins the shared factories to a fixed baseline (`Factory.reset_sequence(0, force=True)`) before every one of its own tests, making its output self-determined regardless of suite composition, collection order, or how many other tests ran first | upstream, proxies-at-home (any `factory_boy` + snapshot-testing pairing, or any suite with process-global ID/name generators) | CLEAN (pattern-level — the technique itself imports nothing fork-specific; its current call site is `test_views.py`, which does assert some fork-only fields elsewhere in the same file, so this is a pattern to replicate in a fresh file, not a file to lift wholesale — see note) | — | ## Docs tooling & federation