chore: 단일 verify 게이트 신설 (pytest.ini + ruff.toml + verify.ps1)

린트와 테스트가 각각 수동 실행이었고, 어느 조합이 커밋 게이트인지 코드에
기록된 곳이 없었다. ruff 는 dev 의존성에도 없어 전역 환경에서만 돌고 있었다.

- verify.ps1: ruff -> pytest 순서로 도는 단일 명령. -SkipLint / -Coverage
- pytest.ini: testpaths, pythonpath(. src) 로 맨 pytest 도 동작, 300s 행 가드
- ruff.toml: py313 / line-length 120 / 기본 룰셋 명시 (ruff 업그레이드가
  게이트를 조용히 넓히거나 좁히지 못하게)
- requirements-dev.txt 에 ruff==0.12.0 핀

린트 18건 정리 (전부 무해하지만 게이트가 통과해야 하므로):
- env_clients.py / store_enrichment.py: logger 정의를 임포트 아래로 (E402 15건)
- test_sqlite_store.py: lambda 할당 -> def (E731 2건)
- test_store_workflow.py: 미사용 임포트 제거 (F401)
This commit is contained in:
changukyu 2026-08-08 09:47:57 +09:00
parent fa00f382e2
commit bc14cd2177
8 changed files with 96 additions and 7 deletions

15
pytest.ini Normal file
View file

@ -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

View file

@ -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

10
ruff.toml Normal file
View file

@ -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"]

View file

@ -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

View file

@ -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:

View file

@ -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"},

View file

@ -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):

57
verify.ps1 Normal file
View file

@ -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