"""Answer-library staleness. The library already knew when an answer was DUE for review. It had no way to know the answer had become WRONG — an entry claiming "MFA is enforced on all privileged accounts" would sit until its next review cycle after the MFA control went red, and be sent to a customer in the meantime. The other half of the design is what these tests also pin down: the job marks and clears, and never rewrites an answer. Automatic re-drafting would push AI text nobody has read into a document that goes to a customer. """ from __future__ import annotations import uuid from datetime import UTC, datetime, timedelta import pytest from sqlalchemy import select from app.domains.auditor.answer_freshness import ( STALE_CONTROL_FAILING, STALE_INTERVAL_ELAPSED, evaluate_answer_freshness, ) from app.domains.auditor.models import ( AuditorAnswerLibraryEntry, ComplianceControl, ComplianceControlStatus, ) async def _mk_control(db, *, passing: bool, status: str | None = None) -> str: key = f"CTL_{uuid.uuid4().hex[:6]}" control = ComplianceControl( key=key, name="Control", category="access", signal_type="detective", enabled=True, ) db.add(control) await db.flush() db.add(ComplianceControlStatus( control_id=control.id, status=status or ("pass" if passing else "fail"), evaluated_at=datetime.now(UTC), )) await db.flush() return key async def _mk_answer( db, *, backing=None, months=12, last_reviewed=None, ) -> AuditorAnswerLibraryEntry: entry = AuditorAnswerLibraryEntry( category="security", question_zh="是否对特权账号强制 MFA?", question_en="Is MFA enforced on privileged accounts?", answer_zh="是。", answer_en="Yes.", status="active", review_interval_months=months, last_reviewed_at=last_reviewed, backing_control_keys=backing, ) db.add(entry) await db.flush() return entry @pytest.mark.asyncio async def test_failing_backing_control_marks_the_answer(db_session): """The urgent case: the claim may now be false.""" key = await _mk_control(db_session, passing=False) entry = await _mk_answer( db_session, backing=[key], last_reviewed=datetime.now(UTC), ) counts = await evaluate_answer_freshness(db_session) await db_session.refresh(entry) assert counts["marked"] == 1 assert entry.stale_reason == STALE_CONTROL_FAILING assert entry.stale_since is not None @pytest.mark.asyncio async def test_passing_backing_control_leaves_it_alone(db_session): key = await _mk_control(db_session, passing=True) entry = await _mk_answer( db_session, backing=[key], last_reviewed=datetime.now(UTC), ) await evaluate_answer_freshness(db_session) await db_session.refresh(entry) assert entry.stale_reason is None @pytest.mark.asyncio async def test_elapsed_review_interval_marks_the_answer(db_session): entry = await _mk_answer( db_session, months=3, last_reviewed=datetime.now(UTC) - timedelta(days=200), ) await evaluate_answer_freshness(db_session) await db_session.refresh(entry) assert entry.stale_reason == STALE_INTERVAL_ELAPSED @pytest.mark.asyncio async def test_a_recent_review_is_not_stale(db_session): entry = await _mk_answer( db_session, months=12, last_reviewed=datetime.now(UTC), ) await evaluate_answer_freshness(db_session) await db_session.refresh(entry) assert entry.stale_reason is None @pytest.mark.asyncio async def test_never_reviewed_counts_as_due_not_as_wrong(db_session): """Reporting "never reviewed" as a failing claim would bury the entries that genuinely are wrong.""" entry = await _mk_answer(db_session, months=6, last_reviewed=None) await evaluate_answer_freshness(db_session) await db_session.refresh(entry) assert entry.stale_reason == STALE_INTERVAL_ELAPSED @pytest.mark.asyncio async def test_failing_control_outranks_an_elapsed_interval(db_session): """One means "possibly wrong", the other only "nobody looked lately" — a reviewer triages them differently.""" key = await _mk_control(db_session, passing=False) entry = await _mk_answer( db_session, backing=[key], months=1, last_reviewed=datetime.now(UTC) - timedelta(days=400), ) await evaluate_answer_freshness(db_session) await db_session.refresh(entry) assert entry.stale_reason == STALE_CONTROL_FAILING @pytest.mark.asyncio async def test_recovery_clears_the_mark(db_session): key = await _mk_control(db_session, passing=False) entry = await _mk_answer( db_session, backing=[key], last_reviewed=datetime.now(UTC), ) await evaluate_answer_freshness(db_session) await db_session.refresh(entry) assert entry.stale_reason == STALE_CONTROL_FAILING status = (await db_session.execute( select(ComplianceControlStatus) .join( ComplianceControl, ComplianceControl.id == ComplianceControlStatus.control_id, ) .where(ComplianceControl.key == key) )).scalar_one() status.status = "pass" await db_session.flush() counts = await evaluate_answer_freshness(db_session) await db_session.refresh(entry) assert counts["cleared"] == 1 assert entry.stale_reason is None assert entry.stale_since is None @pytest.mark.asyncio async def test_stale_since_survives_an_escalation(db_session): """How long it has been stale is the number a reviewer triages on; resetting it when the reason escalates would hide the age.""" entry = await _mk_answer( db_session, months=1, last_reviewed=datetime.now(UTC) - timedelta(days=90), ) await evaluate_answer_freshness(db_session) await db_session.refresh(entry) original = entry.stale_since assert original is not None key = await _mk_control(db_session, passing=False) entry.backing_control_keys = [key] await db_session.flush() await evaluate_answer_freshness(db_session) await db_session.refresh(entry) assert entry.stale_reason == STALE_CONTROL_FAILING assert entry.stale_since == original @pytest.mark.asyncio async def test_rerunning_is_idempotent(db_session): key = await _mk_control(db_session, passing=False) await _mk_answer(db_session, backing=[key], last_reviewed=datetime.now(UTC)) first = await evaluate_answer_freshness(db_session) second = await evaluate_answer_freshness(db_session) assert first["marked"] == 1 assert second["marked"] == 0 assert second["unchanged"] == 1 @pytest.mark.asyncio async def test_inactive_entries_are_skipped(db_session): key = await _mk_control(db_session, passing=False) entry = await _mk_answer(db_session, backing=[key]) entry.status = "archived" await db_session.flush() counts = await evaluate_answer_freshness(db_session) assert counts["checked"] == 0 @pytest.mark.asyncio async def test_zero_interval_means_no_scheduled_review(db_session): """An entry that opts out of periodic review must not be marked for the passage of time alone.""" entry = await _mk_answer( db_session, months=0, last_reviewed=datetime.now(UTC) - timedelta(days=999), ) await evaluate_answer_freshness(db_session) await db_session.refresh(entry) assert entry.stale_reason is None @pytest.mark.asyncio async def test_the_job_never_rewrites_an_answer(db_session): """The design line: drafting may be automatic, sending is always human — and re-drafting an answer without a human is the same thing as sending unreviewed text, one step earlier. """ key = await _mk_control(db_session, passing=False) entry = await _mk_answer( db_session, backing=[key], last_reviewed=datetime.now(UTC), ) before_zh, before_en = entry.answer_zh, entry.answer_en await evaluate_answer_freshness(db_session) await db_session.refresh(entry) assert entry.answer_zh == before_zh assert entry.answer_en == before_en import inspect from app.domains.auditor import answer_freshness source = inspect.getsource(answer_freshness) assert "answer_zh" not in source.replace("answer_zh, entry.answer_en", "") assert "BailianLLMClient" not in source assert "match_and_draft" not in source @pytest.mark.asyncio async def test_job_registered_in_both_processes(): from pathlib import Path from app.scheduled_jobs.registry import get_handler assert get_handler("auditor.answer_library_freshness") is not None root = Path(__file__).resolve().parents[2] / "app" for process in ("main.py", "worker.py"): assert "posture_summary_handler" in ( root / process ).read_text(encoding="utf-8"), process @pytest.mark.parametrize("status", ["unknown", "not_configured", "stale"]) @pytest.mark.asyncio async def test_unevaluated_control_is_not_the_urgent_tier(db_session, status): """"Nobody has established anything" is not "the claim is false". The control vocabulary is pass / fail / stale / not_configured / unknown. Treating everything that is not ``pass`` as urgent meant that on first deploy — when most controls have never run — every answer got the red tier, burying the handful whose control actually went red. That is precisely what the two-tier priority exists to prevent. """ key = await _mk_control(db_session, passing=False, status=status) entry = await _mk_answer( db_session, backing=[key], last_reviewed=datetime.now(UTC), ) await evaluate_answer_freshness(db_session) await db_session.refresh(entry) assert entry.stale_reason is None @pytest.mark.asyncio async def test_a_genuinely_failing_control_is_still_urgent(db_session): """The mirror of the test above: narrowing the tier must not have narrowed it to nothing.""" key = await _mk_control(db_session, passing=False, status="fail") entry = await _mk_answer( db_session, backing=[key], last_reviewed=datetime.now(UTC), ) await evaluate_answer_freshness(db_session) await db_session.refresh(entry) assert entry.stale_reason == STALE_CONTROL_FAILING