mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
Completes the note(scope='handoff') write-path (WIP 23e6ee57): every role
with a dedicated note section is now obligated to populate it, the same way
journals are obligated.
Obligations (foundation.policy.tracing):
- DEV_NOTES / PR_REVIEWER_NOTES / QUICK_CONTEXT_MIN_CHARS requirements +
checkers, wired onto i_am_done (dev_notes), delegate (quick_context), and
pr_pass / pr_fail / post_pr_review (pr_reviewer_notes).
- Fixes a latent bug: the docs-notes checker read dev_notes instead of
doc_notes (the documenter's section); the i_documented shim now feeds
doc_notes to match.
Auditor: a session-scoped note obligation on i_am_idle — the auditor owns no
delivery task and has no delivery verb, so it must have recorded an
observation within the window before going idle (JournalService.has_recent_entry).
Write-then-gate: persisted sections (dev_notes / quick_context) are
pre-written by the agent's note(scope='handoff') before the gated verb;
argument-borne sections (doc_notes / pr_reviewer_notes) are checked through a
SimpleNamespace shim, the same pattern qa_notes already uses.
Config: dev/pr_reviewer/quick_context min-chars (40/40/30), panel-tunable.
Plus per-gap remediation hints and full coverage (write-path routing,
ownership, validation->remediation, each obligation, the doc_notes fix).
Full make-quality green: 9777 passed, 95.6% coverage.
236 lines
7.8 KiB
Python
236 lines
7.8 KiB
Python
"""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",
|
|
}
|
|
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")
|