Files
nisse/nisse-dev/tests/test_gates.py
T
dongarandClaude Opus 5.5 97863c1939
CI / checks (push) Skipped
CI / checks (pull_request) Successful in 1m16s
Bind approvals and feedback to the revision they were about
Fixes counterexamples 1-4 from specs/orchestrator.qnt: a repeated
'approve', 'approve rev-1' meant for the architecture, approving on both
channels, and 'approve' plus an afterthought all landed on the next
plan, one the operator had not seen.

- Revisions are numbered across the work item (A4 already required
  monotonic tags; the code numbered per gate), so rev-N names one plan.
- Each reply is stamped on arrival with the revision that was up: in
  Matrix the one whose approval request had been sent (a plain value
  the transport thread reads, restored on boot), in Gitea the latest
  plan comment older than the reply (board.posts).
- Gate.evaluate applies a reply that names no revision only if its stamp
  is the current revision. Otherwise an approval is not applied and
  feedback rejects nothing (it is still in the transcript), and the
  agent says which revision is up. 'approve rev-N' binds by its number.
- Spec A4 states both rules. The model applies them; scenarios 1-4 now
  expect the properties to hold, and both held over 200,000 random
  traces and Apalache's exhaustive check to depth 8.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PPQ14L3LikyefCMFBqEbKE
2026-09-29 19:13:20 +00:00

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 (A4: 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 (A4): 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
)