Match approvals and commands regardless of case and trailing punctuation
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
This commit is contained in:
dongarandClaude Opus 5.5 committed 2026-09-29 17:57:34 +00:00
1 parent b893fba342
commit 6afd0cc914
5 files changed
+76 -15

No files matched your search

+21
View File
@@ -1,3 +1,5 @@
import pytest
from nisse_dev.gates import Approved, Gate, Ignored, PlanRevision, Rejected
REVISION = PlanRevision(gate="arch", rev=2, content="the plan")
@@ -46,3 +48,22 @@ def test_approve_with_trailing_words_is_rejection() -> None:
# 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"