"""Tier 1 — tracing Requirement enum + check_requirements scaffolding.""" from __future__ import annotations from types import SimpleNamespace import pytest from roboco.foundation.policy import tracing def test_requirement_enum_has_canonical_values() -> None: """Required-set vocabulary mirrors the pre-Phase-2 tracing_gate.py PLUS the new pre-gateway parity additions.""" expected = { "plan", "commits>=1", "pr_open", "progress>=1", "journal:reflect", "journal:decision", "journal:learning", "journal:struggle", "journal:note_at_claim", "journal:decision_at_claim", "journal:during_work>=1", "acceptance_criteria_addressed", "qa_notes>=min", "qa_evidence_inspected", "docs_notes>=min", "docs_files_non_empty", "self_verified", "notes>=min", "subtasks_terminal", "dev_notes>=min", "pr_reviewer_notes>=min", "quick_context>=min", "findings_addressed", } actual = {r.value for r in tracing.Requirement} assert actual == expected, f"Requirement drift: {actual ^ expected}" def test_gate_context_has_journal_presence_flags() -> None: ctx = tracing.GateContext() assert ctx.journal_reflect_present is False assert ctx.journal_decision_present is False assert ctx.journal_learning_present is False assert ctx.journal_struggle_present is False assert ctx.journal_note_at_claim_present is False assert ctx.journal_during_work_count == 0 def test_gate_result_has_passed_and_missing() -> None: result = tracing.GateResult(passed=True) assert result.passed is True assert result.missing == [] def test_docs_notes_checker_reads_doc_notes_not_dev_notes() -> None: """Regression: _check_docs_notes_min_chars must read ``doc_notes`` (the documenter's section), not ``dev_notes`` (the developer's).""" ctx = tracing.GateContext(docs_notes_min_chars=20) # A long dev_notes must NOT satisfy the docs requirement. only_dev = SimpleNamespace(dev_notes="x" * 50, doc_notes="") assert tracing._check_docs_notes_min_chars(only_dev, ctx) == ["docs_notes>=min"] # A long doc_notes satisfies it. has_doc = SimpleNamespace(dev_notes="", doc_notes="y" * 25) assert tracing._check_docs_notes_min_chars(has_doc, ctx) == [] def test_note_section_checkers_read_their_own_fields() -> None: ctx = tracing.GateContext() # defaults: dev 40, pr_reviewer 40, quick_context 30 assert ( tracing._check_dev_notes_min_chars(SimpleNamespace(dev_notes="z" * 40), ctx) == [] ) assert tracing._check_dev_notes_min_chars( SimpleNamespace(dev_notes="z" * 39), ctx ) == ["dev_notes>=min"] assert ( tracing._check_pr_reviewer_notes_min_chars( SimpleNamespace(pr_reviewer_notes="z" * 40), ctx ) == [] ) assert tracing._check_quick_context_min_chars( SimpleNamespace(quick_context="z" * 29), ctx ) == ["quick_context>=min"] def test_note_section_obligations_wired_to_verbs() -> None: assert tracing.Requirement.DEV_NOTES_MIN_CHARS in tracing.requirements_for( "i_am_done" ) assert tracing.Requirement.QUICK_CONTEXT_MIN_CHARS in tracing.requirements_for( "delegate" ) for verb in ("pr_pass", "pr_fail", "post_pr_review"): assert tracing.Requirement.PR_REVIEWER_NOTES_MIN_CHARS in ( tracing.requirements_for(verb) ) def test_check_requirements_passes_when_all_satisfied() -> None: task = SimpleNamespace( plan={"x": 1}, commits=[{"sha": "abc"}], pr_number=42, progress_updates=[{"message": "x"}], acceptance_criteria=[], acceptance_criteria_status=[], ) ctx = tracing.GateContext(journal_reflect_present=True) result = tracing.check_requirements( task=task, requirements=[ tracing.Requirement.PLAN, tracing.Requirement.COMMITS_AT_LEAST_ONE, tracing.Requirement.PR_OPEN, tracing.Requirement.JOURNAL_REFLECT, ], ctx=ctx, ) assert result.passed is True def test_check_requirements_returns_missing_keys_on_failure() -> None: task = SimpleNamespace( plan=None, commits=[], pr_number=None, progress_updates=[], acceptance_criteria=[], acceptance_criteria_status=[], ) result = tracing.check_requirements( task=task, requirements=[ tracing.Requirement.PLAN, tracing.Requirement.COMMITS_AT_LEAST_ONE, tracing.Requirement.PR_OPEN, tracing.Requirement.JOURNAL_REFLECT, ], ) assert result.passed is False assert "plan" in result.missing assert "commits>=1" in result.missing assert "pr_open" in result.missing assert "journal:reflect" in result.missing def test_acceptance_criteria_check_treats_reflect_note_as_addressing_artifact() -> None: """Spec §9 item 1: reflect-note clears the criteria gate.""" task = SimpleNamespace( acceptance_criteria=["AC1", "AC2"], acceptance_criteria_status=[], ) ctx = tracing.GateContext(journal_reflect_present=True) result = tracing.check_requirements( task=task, requirements=[tracing.Requirement.ACCEPTANCE_CRITERIA_ADDRESSED], ctx=ctx, ) assert result.passed is True def test_during_work_count_satisfies_requirement() -> None: task = SimpleNamespace() ctx = tracing.GateContext(journal_during_work_count=1) result = tracing.check_requirements( task=task, requirements=[tracing.Requirement.JOURNAL_DURING_WORK_AT_LEAST_ONE], ctx=ctx, ) assert result.passed is True def test_verb_requirements_covers_pm_decision_chain() -> None: """The 6 inline journal:decision callsites' verbs all require it.""" for verb in ( "submit_up", "complete", "unblock", "escalate_up", "escalate_to_ceo", "delegate", ): reqs = tracing.requirements_for(verb) assert tracing.Requirement.JOURNAL_DECISION in reqs, ( f"{verb} should require journal:decision per spec §11" ) def test_verb_requirements_covers_dev_completion_chain() -> None: reqs = tracing.requirements_for("i_am_done") assert tracing.Requirement.COMMITS_AT_LEAST_ONE in reqs assert tracing.Requirement.PR_OPEN in reqs assert tracing.Requirement.PROGRESS_AT_LEAST_ONE in reqs assert tracing.Requirement.JOURNAL_REFLECT in reqs assert tracing.Requirement.JOURNAL_DURING_WORK_AT_LEAST_ONE in reqs assert tracing.Requirement.ACCEPTANCE_CRITERIA_ADDRESSED in reqs def test_verb_requirements_includes_pre_gateway_parity_at_claim() -> None: assert tracing.Requirement.JOURNAL_NOTE_AT_CLAIM in tracing.requirements_for( "i_will_work_on" ) assert tracing.Requirement.JOURNAL_DECISION_AT_CLAIM in tracing.requirements_for( "i_will_plan" ) def test_pm_complete_requires_both_decision_and_reflect() -> None: """Pre-gateway parity P4: PMs wrote both before complete.""" reqs = tracing.requirements_for("complete") assert tracing.Requirement.JOURNAL_DECISION in reqs assert tracing.Requirement.JOURNAL_REFLECT in reqs def test_qa_pass_review_requires_learning() -> None: reqs = tracing.requirements_for("pass_review") assert tracing.Requirement.QA_NOTES_MIN_CHARS in reqs assert tracing.Requirement.QA_EVIDENCE_INSPECTED in reqs assert tracing.Requirement.JOURNAL_LEARNING in reqs def test_i_am_blocked_requires_struggle_journal() -> None: """Lifts JOURNAL_STRUGGLE out of dangling-enum status.""" assert tracing.Requirement.JOURNAL_STRUGGLE in tracing.requirements_for( "i_am_blocked" ) def test_requirements_for_unknown_verb_raises_key_error() -> None: with pytest.raises(KeyError): tracing.requirements_for("not_a_real_verb")