5c01fe338b
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.
55 lines
1.6 KiB
Go
55 lines
1.6 KiB
Go
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)
|
|
}
|
|
}
|
|
}
|