Give a suppressed rule a durable semantic identity (V-687)
The digest needs to know whether a candidate is already pending before it pays the phraser, and prose is not identity: phrasing varies, and State.Now advancing does not turn the same unmet condition into a new event. A rule eligible for the digest declares DigestIdentity beside its predicate. DigestCandidateFingerprint frames the rule name and severity around it so two rules cannot alias on a shared fact. BreakRule anchors on the last completed break, not on desk_active, which the poller refreshes without the unmet need changing. A rule that declares no identity does not enter the digest, since a generic state hash would either change every tick or ignore an input the rule reads.
This commit is contained in:
@@ -0,0 +1,54 @@
|
||||
package loop
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/kami/maven/internal/store"
|
||||
)
|
||||
|
||||
func TestBreakDigestFingerprintTracksOccurrenceNotTickTime(t *testing.T) {
|
||||
now := refTime()
|
||||
lastBreak := store.Fact{
|
||||
ID: 41, Ts: now.Add(-2 * time.Hour), Kind: store.KindSelf,
|
||||
Key: "break", Value: "done", Source: "tap:voice", Confidence: 1,
|
||||
}
|
||||
state := State{Now: now, Facts: map[string]store.Fact{"break": lastBreak}}
|
||||
|
||||
first, ok := DigestCandidateFingerprint(BreakRule(), state)
|
||||
if !ok || first == "" {
|
||||
t.Fatal("break rule did not produce a digest fingerprint")
|
||||
}
|
||||
state.Now = now.Add(20 * time.Minute)
|
||||
second, ok := DigestCandidateFingerprint(BreakRule(), state)
|
||||
if !ok || second != first {
|
||||
t.Fatalf("the same break occurrence changed with tick time: %q then %q", first, second)
|
||||
}
|
||||
|
||||
newBreak := lastBreak
|
||||
newBreak.ID++
|
||||
newBreak.Ts = now.Add(-100 * time.Minute)
|
||||
state.Facts["break"] = newBreak
|
||||
third, ok := DigestCandidateFingerprint(BreakRule(), state)
|
||||
if !ok || third == first {
|
||||
t.Fatal("a new last-break observation did not change candidate meaning")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDigestCandidateFingerprintFailsClosedWithoutRuleIdentity(t *testing.T) {
|
||||
r := Rule{Name: "future-care-rule", Severity: Sev2, Predicate: func(State) bool { return true }}
|
||||
if got, ok := DigestCandidateFingerprint(r, State{}); ok || got != "" {
|
||||
t.Fatalf("undeclared digest identity must fail closed, got %q, ok=%v", got, ok)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultDigestEligibleRulesDeclareIdentity(t *testing.T) {
|
||||
for _, r := range DefaultRules() {
|
||||
if r.Severity != Sev2 {
|
||||
continue
|
||||
}
|
||||
if r.DigestIdentity == nil {
|
||||
t.Errorf("digest-eligible default rule %q has no durable identity", r.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user