package main import ( "strings" "testing" "time" sdkdelivery "git.novox.be/novox/mesh-sdk/go/delivery" "github.com/novox/mesh-controller/internal/conditions" "github.com/novox/mesh-controller/internal/link" ) func aStuckKey(t *testing.T, bucket, key string) string { t.Helper() k, err := sdkdelivery.StuckKey(bucket, key) if err != nil { t.Fatal(err) } return k } // 1156/AC1: each not-moved stuck key that is not a node's presence is an observation of the delivery scope, kind stuck, // naming the key and what it waited for; a moved key raises nothing, so its condition clears. func TestEachStuckKeyThatHasNotMovedIsAConditionOfTheDeliveryScope(t *testing.T) { // 1156/AC1 since := time.Date(2026, 10, 18, 19, 0, 0, 0, time.UTC) pulse := aStuckKey(t, sdkdelivery.PulseBucket, "x") moved := aStuckKey(t, sdkdelivery.HaveBucket, "ace.zsh") node := aStuckKey(t, sdkdelivery.PresenceBucket, "ace") part := aStuckKey(t, sdkdelivery.PresenceBucket, "part.mesh-controller.one") entries := map[string]sdkdelivery.Stuck{ pulse: {Bucket: sdkdelivery.PulseBucket, Key: "x", Waited: "the build of zsh to be answered", Since: since}, moved: {Bucket: sdkdelivery.HaveBucket, Key: "ace.zsh", Waited: "a want", Since: since, Moved: true}, node: {Bucket: sdkdelivery.PresenceBucket, Key: "ace", Waited: "its return", Since: since}, part: {Bucket: sdkdelivery.PresenceBucket, Key: "part.mesh-controller.one", Waited: "its return", Since: since}, } got := stuckObservations(entries) if len(got) != 2 { t.Fatalf("observations %+v, want the pulse key and the part's presence", got) } byID := map[string]conditions.Observation{} for _, o := range got { if o.Scope != conditions.ScopeDelivery || o.Kind != kindStuck { t.Errorf("%+v is not a stuck condition of the delivery scope", o) } byID[o.ID] = o } o, ok := byID[pulse] if !ok || !strings.Contains(o.Summary, pulse) || !strings.Contains(o.Summary, "the build of zsh to be answered") { t.Fatalf("the condition does not name the key and what it waited for: %+v", got) } if _, ok := byID[part]; !ok { t.Errorf("a part's presence is left out: %v", byID) } // Moved again: the same key, written as moved, raises nothing. entries[pulse] = sdkdelivery.Stuck{Bucket: sdkdelivery.PulseBucket, Key: "x", Since: since, Moved: true} if got := stuckObservations(entries); len(got) != 1 { t.Fatalf("a moved key still raises: %+v", got) } } func aMachineHeardAt(now time.Time, ago time.Duration) machineFacts { return machineFacts{name: "laptop", lastHeard: now.Add(-ago)} } // 1156/AC2: a node's stuck presence key not moved makes the machine silent, unless it sleeps; moved, it is heard; no // timer is armed (the verdict is the key's, whatever the time of the last word). func TestAStuckPresenceKeyIsTheSilentVerdict(t *testing.T) { // 1156/AC2 now := time.Now() f := calm(now) f.machines = append(f.machines, aMachineHeardAt(now, 10*time.Second)) notMoved := sdkdelivery.Stuck{Bucket: sdkdelivery.PresenceBucket, Key: "laptop", Waited: "its return", Since: now.Add(-3 * time.Minute)} applyPresence(&f.machines[1], ¬Moved, nil, now) got := watchHeartbeats(f) if len(got) != 1 || got[0].Machine != "laptop" || got[0].Kind != "silent" { t.Fatalf("a presence key not moved: %+v", got) } // Asleep: not silent. f.machines[1].power = link.PowerState{State: "sleeping", At: now.Add(-time.Hour)} if got := watchHeartbeats(f); len(got) != 0 { t.Fatalf("a sleeping machine raised %+v", got) } // Moved: heard, whatever the store's last word says. f.machines[1].power = link.PowerState{} f.machines[1].lastHeard = now.Add(-2 * time.Hour) moved := notMoved moved.Moved = true applyPresence(&f.machines[1], &moved, nil, now) if got := watchHeartbeats(f); len(got) != 0 { t.Fatalf("a presence key that moved raised %+v", got) } } // 1156/AC3: with a presence entry that says offline, last heard is the entry's time (1183/AC1); with none, // last_seen. An entry that says online counts as heard now: the watchman writes it only when it changes, so its time is // when the machine connected, and a machine online for hours with no stuck key is not silent, whatever last_seen says. func TestLastHeardFollowsThePresenceEntry(t *testing.T) { // 1156/AC3 now := time.Now() seen := now.Add(-time.Hour) m := machineFacts{name: "laptop", lastHeard: seen} applyPresence(&m, nil, nil, now) if !m.lastHeard.Equal(seen) { t.Fatalf("with no presence entry last heard is %v, want last_seen %v", m.lastHeard, seen) } offlineAt := now.Add(-5 * time.Minute) applyPresence(&m, nil, &presenceEntry{Online: false, At: offlineAt}, now) if !m.lastHeard.Equal(offlineAt) { t.Fatalf("offline since %v: last heard is %v", offlineAt, m.lastHeard) } m = machineFacts{name: "laptop", lastHeard: now.Add(-time.Minute)} applyPresence(&m, nil, &presenceEntry{Online: false, At: now.Add(-time.Hour)}, now) if !m.lastHeard.Equal(now.Add(-time.Hour)) { t.Fatalf("an offline entry's time is last heard, not a report: %v", m.lastHeard) } for _, lastSeen := range []time.Duration{10 * time.Second, 5 * time.Hour} { m := machineFacts{name: "laptop", lastHeard: now.Add(-lastSeen)} applyPresence(&m, nil, &presenceEntry{Online: true, At: now.Add(-6 * time.Hour)}, now) if m.silent(now) { t.Fatalf("a machine online since hours ago (last_seen %v ago) with no stuck key is silent", lastSeen) } } }