123 lines
5.4 KiB
Go
123 lines
5.4 KiB
Go
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), every: time.Minute}
|
|
}
|
|
|
|
// 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 later of last_seen and the entry's time; 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, every: time.Minute}
|
|
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), every: time.Minute}
|
|
applyPresence(&m, nil, &presenceEntry{Online: false, At: now.Add(-time.Hour)}, now)
|
|
if !m.lastHeard.Equal(now.Add(-time.Minute)) {
|
|
t.Fatalf("an older entry replaced a newer last_seen: %v", m.lastHeard)
|
|
}
|
|
for _, lastSeen := range []time.Duration{10 * time.Second, 5 * time.Hour} {
|
|
m := machineFacts{name: "laptop", lastHeard: now.Add(-lastSeen), every: time.Minute}
|
|
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)
|
|
}
|
|
}
|
|
}
|