Files
orchestrator/tests/test_qg0_title_limit.py
claude-bot 0ed05417e6 feat(qg0): configurable QG-0 title limit via ORCH_QG0_TITLE_MAX (default 200)
Replace the hardcoded `len(name) > 80` cap in the QG-0 entry validation
(_qg0_errors) with a configurable Settings.qg0_title_max (env
ORCH_QG0_TITLE_MAX, default 200). The 80-char cap was a hygiene limit, not
structural, so valid 81-200 char titles were rejected without a business
reason. The limit is read dynamically per call and the error text interpolates
the active value.

Graceful degradation (AC-3, self-hosting safety): an empty/non-numeric env
value no longer crashes the process on startup. A field_validator(mode="before")
intercepts the raw env before int-parsing and falls back to 200 (never raises),
suppressing pydantic ValidationError.

Additive and backward-compatible (default 200 > old 80). Invariants unchanged:
STAGE_TRANSITIONS, QG_CHECKS registry, DB schema, slug [:30], lower limits,
soft-QG-0 warning path, API.

Refs: ORCH-069

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-08 11:24:01 +00:00

118 lines
4.3 KiB
Python

"""ORCH-069: unit tests for the configurable QG-0 title-length limit.
Covers `_qg0_errors` (src/webhooks/plane.py) reading the upper title limit
dynamically from `settings.qg0_title_max` (env `ORCH_QG0_TITLE_MAX`, default 200),
plus the graceful env-degradation field-validator on `Settings`.
The tests patch `src.config.settings.qg0_title_max` (the same object imported into
`src.webhooks.plane`) and assert boundary behaviour and error texts. For env-driven
cases a FRESH `Settings()` instance is created locally, since the module-level
singleton is built once on import.
"""
import re
import pytest
from src.config import Settings, settings
from src.webhooks.plane import _qg0_errors
VALID_DESCRIPTION = "x" * 30 # >= 20 chars, always passes the description check
def _title_length_error(errors):
"""Return the title length-limit error string, or None if absent.
The short-title error ('нужно >= 5') and the description error are excluded;
only the 'too long' title error is matched (it contains 'максимум').
"""
for e in errors:
if "Title" in e and "максимум" in e:
return e
return None
# --- AC-1: default limit 200, boundary at 201 ------------------------------
def test_tc01_default_limit_200_boundary_pass(monkeypatch):
"""TC-01: title of exactly 200 chars -> no title length error (PASS)."""
monkeypatch.setattr(settings, "qg0_title_max", 200)
errors = _qg0_errors("x" * 200, VALID_DESCRIPTION)
assert _title_length_error(errors) is None
def test_tc02_default_limit_200_boundary_fail(monkeypatch):
"""TC-02: title of 201 chars -> length error mentioning '200'."""
monkeypatch.setattr(settings, "qg0_title_max", 200)
errors = _qg0_errors("x" * 201, VALID_DESCRIPTION)
err = _title_length_error(errors)
assert err is not None
assert "200" in err
# --- AC-2: configurable limit 120, boundary at 121 -------------------------
def test_tc03_custom_limit_120_boundary_pass(monkeypatch):
"""TC-03: with limit 120, a 120-char title passes."""
monkeypatch.setattr(settings, "qg0_title_max", 120)
errors = _qg0_errors("x" * 120, VALID_DESCRIPTION)
assert _title_length_error(errors) is None
def test_tc04_custom_limit_120_boundary_fail(monkeypatch):
"""TC-04: with limit 120, a 121-char title fails; text mentions 120 not 80."""
monkeypatch.setattr(settings, "qg0_title_max", 120)
errors = _qg0_errors("x" * 121, VALID_DESCRIPTION)
err = _title_length_error(errors)
assert err is not None
assert "120" in err
assert "80" not in err
# --- AC-3: graceful handling of invalid/empty env --------------------------
def test_tc05_graceful_non_numeric_env(monkeypatch):
"""TC-05: non-numeric env -> Settings() does not raise, limit == 200."""
monkeypatch.setenv("ORCH_QG0_TITLE_MAX", "abc")
s = Settings()
assert s.qg0_title_max == 200
def test_tc06_graceful_empty_env(monkeypatch):
"""TC-06: empty-string env -> default 200, no exception."""
monkeypatch.setenv("ORCH_QG0_TITLE_MAX", "")
s = Settings()
assert s.qg0_title_max == 200
def test_tc07_valid_numeric_env(monkeypatch):
"""TC-07: valid numeric env -> the given value is applied (positive path)."""
monkeypatch.setenv("ORCH_QG0_TITLE_MAX", "150")
s = Settings()
assert s.qg0_title_max == 150
# --- AC-4: lower limits unchanged ------------------------------------------
def test_tc08_short_title_still_errors(monkeypatch):
"""TC-08: title < 5 chars still raises the short-title error."""
monkeypatch.setattr(settings, "qg0_title_max", 200)
errors = _qg0_errors("abc", VALID_DESCRIPTION)
assert any("Title" in e and "нужно >= 5" in e for e in errors)
def test_tc09_short_description_still_errors(monkeypatch):
"""TC-09: description < 20 chars still raises the short-description error."""
monkeypatch.setattr(settings, "qg0_title_max", 200)
errors = _qg0_errors("Valid title", "short")
assert any("Description" in e for e in errors)
# --- AC-7: backward compatibility ------------------------------------------
def test_tc10_backward_compat_titles_81_to_200(monkeypatch):
"""TC-10: a title previously rejected by the 80-char cap now passes at 200."""
monkeypatch.setattr(settings, "qg0_title_max", 200)
errors = _qg0_errors("x" * 100, VALID_DESCRIPTION)
assert _title_length_error(errors) is None