feat(rules): the write path carries a rule's check, and empty finally means empty (#3096, milestone 312 step 2)
CI & Build / Plugin hooks (push) Successful in 8s
CI & Build / Python lint (push) Successful in 3s
CI & Build / TypeScript typecheck (push) Successful in 33s
CI & Build / Python tests (push) Failing after 45s
CI & Build / Build & push image (push) Skipped
CI & Build / integration (push) Successful in 29s
CI & Build / Plugin hooks (push) Successful in 8s
CI & Build / Python lint (push) Successful in 3s
CI & Build / TypeScript typecheck (push) Successful in 33s
CI & Build / Python tests (push) Failing after 45s
CI & Build / Build & push image (push) Skipped
CI & Build / integration (push) Successful in 29s
verify_with / expires_when now reach a rule through both doors and come back on every read. The open question this step existed to settle was how to UNSET a nullable field, and the answer is one convention per door: - MCP: "" still means "leave unchanged" — an agent filling three fields must not wipe the other five — so clearing is explicit, clear_fields=["..."]. Naming the field is the one form that cannot happen by accident. - REST: a cleared form input arrives as "", and the service normalises "" to NULL for every nullable rule column, so an emptied input does what it looks like it does. Two idioms, one outcome, and the normalisation is what makes the step-3 sweep correct: `verify_with IS NOT NULL` would otherwise be true for every rule ever touched through the UI, and the sweep would list the whole rulebook and mean nothing. to_dict renders "" and NULL identically, so this is only visible against a real column — hence the integration module rather than a mock. Editing verify_with drops verified_at. A stamp certifies A CHECK, not a rule; reword the check and the old stamp vouches for something that no longer exists. Safe direction, same asymmetry as _valid_tier: a rule wrongly listed as due costs one look, a rule wrongly vouched for costs the thing the sweep exists to catch. Editing anything else leaves the stamp alone, or a rulebook tidy-up would reset every constraint and the ordering would carry nothing. Reads: rule_brief attaches `last_verified` ONLY to a rule that carries a check — its presence is the signal, and it says both "this asserts a fact that can go false" and "here is how long ago anyone confirmed it". "never" rather than null, per #2483. The check text itself stays in get_rule; a listing needs to know which rules can rot, not how to test them. Search hits carry the full trio, since a hit is exactly the moment someone is about to act on a rule. Also folds in the #3078 finding, which had been sitting as a note: create_rule now teaches that when_to_apply is the retrieval surface and must carry the SYMPTOM — the words you would type while stuck — not just the situation. fake_rule gains the three fields as None for the reason the helper already documents one line up: unnamed, verify_with is a truthy MagicMock and every stand-in rule would claim a check it does not have. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -392,3 +392,57 @@ def test_an_unknown_tier_falls_back_to_binding():
|
||||
assert _valid_tier("Conditional") == "always_on"
|
||||
assert _valid_tier("") == "always_on"
|
||||
assert _valid_tier("occasionally") == "always_on"
|
||||
|
||||
|
||||
# ── verify_with / expires_when (milestone 312) ──────────────────────────
|
||||
|
||||
def test_a_rule_with_no_check_says_nothing_about_verification():
|
||||
"""The empty case is the COMMON case, and it must stay silent.
|
||||
|
||||
Most rules are decisions: they have no truth value and there is nothing to
|
||||
go and check. If a brief carried `last_verified` for those too, the signal
|
||||
would be worthless — every rule would look like something someone ought to
|
||||
be verifying, and the handful that genuinely rot would stop standing out.
|
||||
"""
|
||||
from scribe.services.rulebooks import last_verified_label, rule_brief
|
||||
|
||||
rule = fake_rule()
|
||||
assert last_verified_label(rule) is None
|
||||
assert "last_verified" not in rule_brief(rule)
|
||||
|
||||
|
||||
def test_an_unverified_constraint_reads_never_rather_than_null():
|
||||
"""#2483 again: a null key reads as a capability going unused. "never" is
|
||||
a different and much stronger claim — this rule asserts a fact about
|
||||
someone else's software and nobody has ever confirmed it."""
|
||||
from scribe.services.rulebooks import last_verified_label, rule_brief
|
||||
|
||||
rule = fake_rule(verify_with="cat CI-runner/renovate/config.js")
|
||||
assert last_verified_label(rule) == "never"
|
||||
assert rule_brief(rule)["last_verified"] == "never"
|
||||
|
||||
|
||||
def test_a_verified_constraint_reports_the_date_it_was_checked():
|
||||
"""A date, not a stamp — the question is "how old is this", the same call
|
||||
rule_brief makes for updated_at."""
|
||||
from scribe.services.rulebooks import last_verified_label
|
||||
|
||||
rule = fake_rule(
|
||||
verify_with="cat CI-runner/renovate/config.js",
|
||||
verified_at=datetime(2026, 8, 27, 11, 46, tzinfo=timezone.utc),
|
||||
)
|
||||
assert last_verified_label(rule) == "2026-08-27"
|
||||
|
||||
|
||||
def test_the_check_text_itself_never_enters_a_listing():
|
||||
"""A listing says WHICH rules can rot, not how to test them. The check can
|
||||
be a long command; multiplied across an always-on set it is the same bloat
|
||||
`why` and `how_to_apply` are kept out of a brief to avoid."""
|
||||
from scribe.services.rulebooks import rule_brief
|
||||
|
||||
out = rule_brief(fake_rule(
|
||||
verify_with="a very long command " * 20,
|
||||
expires_when="the runner learns a new shell",
|
||||
))
|
||||
assert "verify_with" not in out
|
||||
assert "expires_when" not in out
|
||||
|
||||
Reference in New Issue
Block a user