CI / checks (push) Skipped
CI / checks (pull_request) Successful in 1m22s
Development Plan M0.1. Every section reference in code, tests, specs/, the example config, the tuning guide, and the CLAUDE.md and README files now cites docs/Architecture.md (P, L, D, O, X, Q) instead of the old spec's A-H, and docs/Architecture-Build-Spec.md is deleted (git keeps it). The plan's old-to-new table goes with it. Mostly mechanical, with a per-site choice where one old section split: A4 became L2 where it meant stop/abandon/the loop and L3 where it meant approvals; A10 became L6 in the context-budget code and L7 elsewhere; B2 became D4 for outbound Gitea and publishing and D5 for review intake; 'A3 step N' became the matching step of L8's boot sequence. Comments describing behaviour the architecture drops (planning always escalating, thresholds tuned at an old build step) now describe the current code and point at the Development Plan. docs/Architecture.md gains one line the rewrite missed: intake outages delay but never fail work (old B2), now in L5, where the poller and Matrix code cite it. Comments and docs only; no behaviour change. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PPQ14L3LikyefCMFBqEbKE
106 lines
3.8 KiB
Python
106 lines
3.8 KiB
Python
import pytest
|
|
|
|
from nisse_dev.gates import Approved, Gate, Ignored, PlanRevision, Rejected
|
|
|
|
REVISION = PlanRevision(gate="arch", rev=2, content="the plan")
|
|
OPERATORS = {"don"}
|
|
|
|
|
|
def gate() -> Gate:
|
|
return Gate(REVISION, OPERATORS)
|
|
|
|
|
|
def test_bare_approve_binds_to_current_revision() -> None:
|
|
result = gate().evaluate("don", "approve", channel="transport")
|
|
assert isinstance(result, Approved)
|
|
assert result.revision.tag == "rev-2"
|
|
assert result.approver == "don"
|
|
|
|
|
|
def test_tagged_approve_matching_current_revision() -> None:
|
|
result = gate().evaluate("don", "approve rev-2", channel="gitea")
|
|
assert isinstance(result, Approved)
|
|
assert result.channel == "gitea"
|
|
|
|
|
|
def test_stale_revision_tag_is_ignored_with_notice() -> None:
|
|
result = gate().evaluate("don", "approve rev-1", channel="transport")
|
|
assert isinstance(result, Ignored)
|
|
assert result.notice is not None
|
|
assert "rev-1" in result.notice
|
|
assert "rev-2" in result.notice
|
|
|
|
|
|
def test_any_other_reply_is_rejection_with_feedback() -> None:
|
|
result = gate().evaluate("don", "too vague, split milestone 1", "transport")
|
|
assert isinstance(result, Rejected)
|
|
assert result.feedback == "too vague, split milestone 1"
|
|
|
|
|
|
def test_unknown_sender_is_silently_ignored() -> None:
|
|
result = gate().evaluate("mallory", "approve", channel="transport")
|
|
assert isinstance(result, Ignored)
|
|
assert result.notice is None
|
|
|
|
|
|
def test_approve_with_trailing_words_is_rejection() -> None:
|
|
# "approve, but rename the module" is feedback, not approval (L3: the
|
|
# typed word approve, nothing else).
|
|
result = gate().evaluate("don", "approve, but rename the module", "transport")
|
|
assert isinstance(result, Rejected)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"text", ["Approve", "APPROVE", "Approve.", "approve!", " Approve rev-2 "]
|
|
)
|
|
def test_approval_ignores_case_and_trailing_punctuation(text: str) -> None:
|
|
# A phone keyboard's "Approve" used to count as rejection feedback (#64).
|
|
assert isinstance(gate().evaluate("don", text, channel="transport"), Approved)
|
|
|
|
|
|
def test_a_capitalised_stale_approval_is_still_refused() -> None:
|
|
result = gate().evaluate("don", "Approve REV-1", channel="transport")
|
|
assert isinstance(result, Ignored)
|
|
|
|
|
|
def test_a_sentence_containing_approve_is_still_feedback() -> None:
|
|
result = gate().evaluate("don", "I approve of most of it", channel="transport")
|
|
assert isinstance(result, Rejected)
|
|
assert result.feedback == "I approve of most of it"
|
|
|
|
|
|
# --- binding by arrival (L3): a reply that names no revision is about the one
|
|
# that was up when it arrived.
|
|
|
|
|
|
def test_a_bare_approval_about_an_earlier_revision_is_not_applied() -> None:
|
|
result = gate().evaluate("don", "approve", channel="transport", about=1)
|
|
assert isinstance(result, Ignored)
|
|
assert "arrived while rev-1 was up" in result.notice
|
|
assert "arch rev-2" in result.notice
|
|
|
|
|
|
def test_feedback_about_an_earlier_revision_is_not_a_rejection() -> None:
|
|
result = gate().evaluate("don", "also add docs", channel="transport", about=1)
|
|
assert isinstance(result, Ignored)
|
|
assert "not taken as feedback on arch rev-2" in result.notice
|
|
|
|
|
|
def test_a_reply_before_any_plan_was_up_is_not_applied() -> None:
|
|
result = gate().evaluate("don", "approve", channel="transport", about=0)
|
|
assert isinstance(result, Ignored)
|
|
assert "no plan" in result.notice
|
|
|
|
|
|
def test_replies_about_the_current_revision_apply() -> None:
|
|
assert isinstance(gate().evaluate("don", "approve", "transport", about=2), Approved)
|
|
result = gate().evaluate("don", "add docs", "transport", about=2)
|
|
assert isinstance(result, Rejected)
|
|
|
|
|
|
def test_a_named_revision_binds_by_its_number() -> None:
|
|
# "approve rev-2" says which plan it means, whenever it arrived.
|
|
assert isinstance(
|
|
gate().evaluate("don", "approve rev-2", "gitea", about=1), Approved
|
|
)
|