diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 0000000..bf883c3 --- /dev/null +++ b/pytest.ini @@ -0,0 +1,15 @@ +[pytest] +testpaths = tests + +# `tests.rights_filter.server.test_sqlite_store` is imported by sibling specs for +# shared fakes, and every module under test lives in src/. Declaring both here +# means bare `pytest` works; without it only `python -m pytest` did (that form +# puts the CWD on sys.path as a side effect, which is easy to get wrong in CI). +pythonpath = . src + +# Hang guard, not a performance budget. The slowest single spec (browser upload +# + real analysis) runs well under a minute; this only fires when something +# deadlocks, so an unattended `verify` run fails instead of hanging forever. +timeout = 300 + +addopts = --strict-markers diff --git a/requirements-dev.txt b/requirements-dev.txt index 0ce3b7e..4dc30b3 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -3,6 +3,9 @@ pytest==8.4.2 pytest-cov==6.0.0 pytest-timeout==2.4.0 +# Lint gate in verify.ps1. Pinned because ruff's default rule set moves between +# releases; ruff.toml states the selection, this states the version. +ruff==0.12.0 # Operator-GUI browser (E2E) tests. The Chromium binary is NOT pip-installed: # on the build/staging host run `playwright install chromium`, then bundle the # browser cache (PLAYWRIGHT_BROWSERS_PATH) into the offline artifact for the diff --git a/ruff.toml b/ruff.toml new file mode 100644 index 0000000..b02d51f --- /dev/null +++ b/ruff.toml @@ -0,0 +1,10 @@ +# Pinned so `verify` produces the same result on every machine. Without a config +# file ruff falls back to its built-in defaults, which shift between releases — +# a lint gate that changes under you is not a gate. +target-version = "py313" +line-length = 120 + +[lint] +# Ruff's default set (pycodestyle errors + pyflakes). Stated explicitly rather +# than inherited so a ruff upgrade cannot silently widen or narrow the gate. +select = ["E4", "E7", "E9", "F"] diff --git a/src/rights_filter/integrations/env_clients.py b/src/rights_filter/integrations/env_clients.py index 7cc4b2f..0ca70b5 100644 --- a/src/rights_filter/integrations/env_clients.py +++ b/src/rights_filter/integrations/env_clients.py @@ -5,8 +5,6 @@ from dataclasses import dataclass from datetime import datetime from typing import Any, Mapping -logger = logging.getLogger(__name__) - from rights_filter.analysis.llm_assistance import ( DEFAULT_MODEL, InternalLlmAssistant, @@ -24,6 +22,8 @@ from rights_filter.integrations.google_custom_search import ( from rights_filter.integrations.naver_search import NaverOpenApiSearchClient, NaverSearchAdapter from rights_filter.integrations.search_policy import SearchApiPolicy +logger = logging.getLogger(__name__) + # Chosen by measurement against real stored evidence (54 rows, 12 selected) on # the GPU target, not by parameter count: # qwen2.5:0.5b-instruct 55.7s — echoed the shape example as its claim text diff --git a/src/rights_filter/server/store_enrichment.py b/src/rights_filter/server/store_enrichment.py index 3ed6f7f..381760a 100644 --- a/src/rights_filter/server/store_enrichment.py +++ b/src/rights_filter/server/store_enrichment.py @@ -15,8 +15,6 @@ from dataclasses import replace from datetime import datetime from typing import Any -logger = logging.getLogger(__name__) - from rights_filter.analysis.face_person_detection import ( HeuristicFacePersonDetector, face_person_evidence_data, @@ -48,6 +46,8 @@ from rights_filter.server.store_serialization import ( _query_history_status, ) +logger = logging.getLogger(__name__) + class StoreEnrichmentMixin: def _sync_provider_payloads(self) -> None: diff --git a/tests/rights_filter/server/test_sqlite_store.py b/tests/rights_filter/server/test_sqlite_store.py index 7f7deb5..b14c7e3 100644 --- a/tests/rights_filter/server/test_sqlite_store.py +++ b/tests/rights_filter/server/test_sqlite_store.py @@ -3171,7 +3171,9 @@ def test_search_result_candidate_image_similarity_respects_runtime_threshold( ): image_store = _png_image_store(tmp_path / "submissions") original_image = image_store.image_payload("SUB-PNG1").content - fetcher = lambda _: original_image + + def fetcher(_): + return original_image runtime = build_provider_runtime( env={"COPYRIGHTER_SEARCH_RESULT_SIMILARITY_THRESHOLD": "0.95"}, @@ -3287,7 +3289,9 @@ def test_sqlite_store_compares_unwrapped_search_result_image_urls_against_submis def test_search_result_similarity_collects_multiple_matches_within_limit(tmp_path: Path): image_store = _png_image_store(tmp_path / "submissions") original_image = image_store.image_payload("SUB-PNG1").content - fetcher = lambda _: original_image + + def fetcher(_): + return original_image runtime = build_provider_runtime( env={"COPYRIGHTER_SEARCH_RESULT_COMPARE_LIMIT": "3"}, diff --git a/tests/rights_filter/server/test_store_workflow.py b/tests/rights_filter/server/test_store_workflow.py index d43fb76..aa7210f 100644 --- a/tests/rights_filter/server/test_store_workflow.py +++ b/tests/rights_filter/server/test_store_workflow.py @@ -7,7 +7,7 @@ from rights_filter.server.sqlite_store import CopyrighterStore from rights_filter.server.store_backup import restore_backup from rights_filter.server.store_workflow import MAX_BATCH_DECISIONS -from tests.rights_filter.server.test_sqlite_store import _image_store, FakeJsonTransport +from tests.rights_filter.server.test_sqlite_store import FakeJsonTransport def _photo_store(root: Path, *, duplicate: bool = False): diff --git a/verify.ps1 b/verify.ps1 new file mode 100644 index 0000000..ca4c9c0 --- /dev/null +++ b/verify.ps1 @@ -0,0 +1,57 @@ +<# +.SYNOPSIS + The single verification gate for POSA Copyrighter. Run this before commit, + before a delivery build, and from CI — same command, same result everywhere. + +.DESCRIPTION + Runs, in order: + 1. ruff — lint (config: ruff.toml) + 2. pytest — full suite incl. operator-GUI E2E (config: pytest.ini) + + Browser (Playwright) specs skip cleanly when Chromium is absent, so this + works on a host that has not run `playwright install chromium`. + +.PARAMETER SkipLint + Run tests only. For a fast inner loop; the commit gate runs both. + +.PARAMETER Coverage + Add a coverage report over src/. + +.EXAMPLE + .\verify.ps1 +.EXAMPLE + .\verify.ps1 -Coverage +#> +[CmdletBinding()] +param( + [switch]$SkipLint, + [switch]$Coverage +) + +$ErrorActionPreference = 'Stop' +Set-Location -LiteralPath $PSScriptRoot + +$failures = @() + +if (-not $SkipLint) { + Write-Host "`n=== ruff check ===" -ForegroundColor Cyan + & python -m ruff check src tests + if ($LASTEXITCODE -ne 0) { $failures += 'ruff' } +} + +Write-Host "`n=== pytest ===" -ForegroundColor Cyan +$pytestArgs = @() +if ($Coverage) { + $pytestArgs += @('--cov=src/rights_filter', '--cov-report=term-missing') +} +& python -m pytest @pytestArgs +if ($LASTEXITCODE -ne 0) { $failures += 'pytest' } + +Write-Host '' +if ($failures.Count -gt 0) { + Write-Host "VERIFY FAILED: $($failures -join ', ')" -ForegroundColor Red + exit 1 +} + +Write-Host 'VERIFY OK' -ForegroundColor Green +exit 0