CI / checks (push) Skipped
CI / checks (pull_request) Successful in 1m15s
A phone keyboard's "Approve" did not match the approval pattern, so it counted as rejection feedback and threw the plan away; "Stop" did not reach the cancel flag; other capitalised commands got a hint instead of acting. gates.command_form() is how a message reads as a command: case, surrounding whitespace, inner runs of whitespace and a trailing '.' or '!' do not matter. The approval gate and every command match in the orchestrator (stop, status, abandon, use, start milestone, next milestone, project complete, the restart choices) use it. Only matching changes: the project name in 'use <name>' keeps its case, and feedback and answers are recorded and passed on as written. A stale revision is still refused, and approval still needs the whole message to be the command. Closes #64. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PPQ14L3LikyefCMFBqEbKE
70 lines
2.4 KiB
Python
70 lines
2.4 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"
|