"""The adoption ledger without a database (milestone 463 step 4). The parts that decide are pure and pinned here: what an assessment needs before it can be recorded, what each ground of the conflict order needs — including that every ground ABOVE the deciding one was said not to apply — when an answer needs a recheck, how a losing side is folded into the note, and which reference an owed task points at. Beside them: the outcomes and the order the agent is told are the ones enforced. The state machine against Postgres is in tests/test_integration_family_adoption.py. """ from __future__ import annotations import pytest from scribe.mcp.server import _READ_ONLY_TOOLS, _WRITE_TOOLS from scribe.services.family_adoption import ( CONFLICT_KEYS, CONFLICT_ORDER, OUTCOME_KEYS, _fold_section, _reference_line, assessment_problems, conflict_problems, needs_recheck, ) from tests.helpers import tool_doc # --- what an assessment needs ------------------------------------------------------ @pytest.mark.parametrize("outcome", OUTCOME_KEYS) def test_every_outcome_needs_a_reason(outcome): problems = assessment_problems(outcome=outcome, reason=" ", evidence=["src/x.py"]) assert len(problems) == 1 and outcome in problems[0] def test_adopted_needs_evidence_naming_where(): assert assessment_problems(outcome="adopted", reason="done", evidence=[]) == [ "adopted needs evidence naming where the project does it"] assert assessment_problems(outcome="adopted", reason="done", evidence=[" "]) != [] assert assessment_problems(outcome="adopted", reason="done", evidence=["ci run 12"]) == [] @pytest.mark.parametrize("outcome", ("exempt", "variant", "owed")) def test_the_other_outcomes_need_no_evidence(outcome): assert assessment_problems(outcome=outcome, reason="a fact", evidence=None) == [] def test_an_unknown_outcome_is_refused_by_name(): problems = assessment_problems(outcome="ignored", reason="x", evidence=[]) assert problems and all(k in problems[0] for k in OUTCOME_KEYS) # --- the conflict order, branch by branch ---------------------------------------- def _checked(upto: str) -> dict: return {k: f"{k} did not decide" for k in CONFLICT_KEYS[:CONFLICT_KEYS.index(upto)]} GOOD = { "operator_stance": dict(evidence=["rule: one keystore per app"]), "covers_failure": dict(evidence=["incident #12: update refused, signature mismatch"]), "split_by_condition": dict(conditions={"canon": "the app self-updates", "other": "a store installs it"}), "most_recent_complete": dict(evidence=["verified on a device 2026-10-01"]), } @pytest.mark.parametrize("ground", CONFLICT_KEYS) def test_each_ground_holds_when_its_needs_are_met(ground): kw = {"evidence": None, "conditions": None, **GOOD[ground]} assert conflict_problems(ground=ground, grounds_checked=_checked(ground), fold="their reasoning", **kw) == [] @pytest.mark.parametrize("ground", CONFLICT_KEYS[1:]) def test_a_ground_is_refused_while_any_ground_above_it_is_unanswered(ground): above = CONFLICT_KEYS[:CONFLICT_KEYS.index(ground)] for skipped in above: checked = {k: v for k, v in _checked(ground).items() if k != skipped} problems = conflict_problems(ground=ground, grounds_checked=checked, fold="x", **{"evidence": None, "conditions": None, **GOOD[ground]}) assert len(problems) == 1 and f"'{skipped}'" in problems[0] def test_the_first_ground_needs_nothing_checked_above_it(): assert conflict_problems(ground="operator_stance", grounds_checked=None, fold="x", evidence=["rule 4"], conditions=None) == [] @pytest.mark.parametrize("ground", ("operator_stance", "covers_failure", "most_recent_complete")) def test_grounds_one_two_and_four_need_their_evidence(ground): problems = conflict_problems(ground=ground, grounds_checked=_checked(ground), fold="x", evidence=[" "], conditions=None) assert len(problems) == 1 and "evidence" in problems[0] def test_a_split_needs_both_conditions(): for conds in (None, {"canon": "self-updates"}, {"canon": "", "other": "store"}): problems = conflict_problems(ground="split_by_condition", grounds_checked=_checked("split_by_condition"), fold="x", evidence=None, conditions=conds) assert len(problems) == 1 and "conditions" in problems[0] def test_the_losing_reasoning_is_required(): problems = conflict_problems(ground="operator_stance", grounds_checked=None, fold=" ", evidence=["rule 4"], conditions=None) assert len(problems) == 1 and "never dropped" in problems[0] def test_an_unknown_ground_names_the_order(): problems = conflict_problems(ground="seniority", grounds_checked=None, fold="x", evidence=[], conditions=None) assert problems == [f"ground must be one of, in order: {', '.join(CONFLICT_KEYS)}"] # --- recheck ------------------------------------------------------------------------- @pytest.mark.parametrize("row_status,row_version,idea_status,idea_version,expected", [ ("adopted", 1, "canon", 2, True), ("owed", 1, "canon", 2, True), ("adopted", 2, "canon", 2, False), # An undone revision leaves answers AHEAD of the idea: still not agreeing. ("adopted", 3, "canon", 2, True), ("unassessed", None, "canon", 2, False), ("adopted", 1, "retired", 2, False), ]) def test_needs_recheck(row_status, row_version, idea_status, idea_version, expected): assert needs_recheck(row_status, row_version, idea_status, idea_version) is expected # --- folding the losing side into the note ------------------------------------------ @pytest.mark.parametrize("ground,heading", [ ("operator_stance", "### Alternative — two's approach"), ("covers_failure", "### Trap — two's approach"), ("most_recent_complete", "### Alternative — two's approach"), ("split_by_condition", "### When a store installs it — two's approach"), ]) def test_the_losing_side_is_folded_as_its_ground_says(ground, heading): text = _fold_section(ground=ground, fold="Their reasoning.", conditions=GOOD["split_by_condition"]["conditions"], canon_title="one", other_title="two", when="2026-10-06") assert text.startswith(heading) assert "Their reasoning." in text and "one's approach" in text # --- the reference an owed task points at ----------------------------------------- REFS = [ {"id": 10, "title": "Update installer", "language": "kotlin"}, {"id": 11, "title": "Update installer", "language": "dart"}, ] def test_the_reference_in_the_projects_language_is_named(): line = _reference_line(REFS, ["dart"], idea_id=5) assert "#11" in line and "#10" not in line def test_no_reference_in_the_language_names_the_ones_that_exist(): line = _reference_line(REFS, ["python"], idea_id=5) assert "python" in line and "#10" in line and "#11" in line def test_no_reference_at_all_points_at_the_idea(): assert "#5" in _reference_line([], ["python"], idea_id=5) # --- the product text and the doors --------------------------------------------------- def test_the_outcomes_the_agent_is_told_are_the_ones_enforced(): """Rule 119: the outcomes and the order are product text. The tool docstrings must name every outcome and every ground the service checks, in the service's order.""" assess_doc = tool_doc("scribe.mcp.tools.family", "assess_family_adoption") for n, key in enumerate(OUTCOME_KEYS, start=1): assert f"{n}. {key} —" in assess_doc, f"outcome {n} is not taught as {key}" resolve_doc = tool_doc("scribe.mcp.tools.family", "resolve_family_conflict") for n, key in enumerate(CONFLICT_KEYS, start=1): assert f"{n}. {key} —" in resolve_doc, f"ground {n} is not taught as {key}" assert [c["fold_as"] for c in CONFLICT_ORDER] == ["alternative", "trap", "condition", "alternative"] def test_every_adoption_tool_is_classified(): reads = {"get_family_adoption", "list_family_adoptions"} writes = {"assess_family_adoption", "resolve_family_conflict", "revise_family_idea", "set_family_references", "set_family_topic"} assert reads <= _READ_ONLY_TOOLS assert writes <= _WRITE_TOOLS