package seatwait import ( "errors" "testing" "time" "git.novox.be/novox/mesh-sdk/go/seatlist" "git.novox.be/novox/mesh-sdk/go/views" ) type fakeReader struct{ st seatlist.Status } func (f *fakeReader) Status() seatlist.Status { return f.st } type fakeTimer struct { c *fakeClock due time.Time f func() stopped bool } func (t *fakeTimer) Stop() bool { was := !t.stopped; t.stopped = true; return was } type fakeClock struct { now time.Time timers []*fakeTimer } func (c *fakeClock) Now() time.Time { return c.now } func (c *fakeClock) AfterFunc(d time.Duration, f func()) Timer { t := &fakeTimer{c: c, due: c.now.Add(d), f: f} c.timers = append(c.timers, t) return t } // advance moves the clock and fires the timers that fell due and were not stopped. func (c *fakeClock) advance(d time.Duration) { c.now = c.now.Add(d) for _, t := range c.timers { if !t.stopped && !t.due.After(c.now) { t.stopped = true t.f() } } } type fakeFindings struct { put, deleted []views.Finding err error } func (f *fakeFindings) Put(x views.Finding) error { if f.err != nil { return f.err } f.put = append(f.put, x) return nil } func (f *fakeFindings) Delete(x views.Finding) error { f.deleted = append(f.deleted, x); return nil } // ADR 0636 §1: a reader that waited five minutes makes the node write one finding, which the first full read clears. func caseTheNodeWritesOneFindingAfterFiveMinutesAndClearsItAtTheFirstFullRead(t *testing.T) { reader := &fakeReader{st: seatlist.Status{Heard: false, LastError: "no answer"}} clock := &fakeClock{now: time.Date(2026, 11, 5, 12, 0, 0, 0, time.UTC)} findings := &fakeFindings{} var said []string w := &Waiter{Node: "ace", Observer: "node-engine", Reader: reader, Findings: findings, Clock: clock, Say: func(s string) { said = append(said, s) }} w.Changed() clock.advance(Bound - time.Second) if len(findings.put) != 0 { t.Fatalf("a finding was written before the bound: %+v", findings.put) } w.Changed() // another change while waiting starts no second bound clock.advance(time.Second) if len(findings.put) != 1 || findings.put[0].Kind != Kind || findings.put[0].ID != "ace" { t.Fatalf("after five minutes the node wrote %+v, want one %s finding for ace", findings.put, Kind) } clock.advance(time.Hour) if len(findings.put) != 1 { t.Fatalf("the finding was written again: %d", len(findings.put)) } reader.st = seatlist.Status{Heard: true} w.Changed() if len(findings.deleted) != 1 || findings.deleted[0].ID != "ace" { t.Fatalf("the first full read left the finding: deleted %+v", findings.deleted) } if on, _ := w.Waiting(); on { t.Error("the waiter still waits after the full read") } } // A read that ends the wait stops the bound; a timer that fires after the list was read writes nothing. func caseAReadWithinTheBoundWritesNothing(t *testing.T) { reader := &fakeReader{st: seatlist.Status{Heard: false}} clock := &fakeClock{now: time.Date(2026, 11, 5, 12, 0, 0, 0, time.UTC)} findings := &fakeFindings{} w := &Waiter{Node: "ace", Observer: "node-engine", Reader: reader, Findings: findings, Clock: clock} w.Changed() clock.advance(time.Minute) reader.st = seatlist.Status{Heard: true} w.Changed() clock.advance(time.Hour) if len(findings.put) != 0 || len(findings.deleted) != 0 { t.Fatalf("a list read in time wrote %+v and cleared %+v", findings.put, findings.deleted) } // The seat answered the list already held: no change was said, so the timer is the one to look again. reader.st = seatlist.Status{Heard: false} w.Changed() reader.st = seatlist.Status{Heard: true} clock.advance(Bound) if len(findings.put) != 0 { t.Fatalf("a timer that found the list read wrote %+v", findings.put) } } // A finding that cannot be written is said and tried again at the next change, not lost. func caseAFindingThatCannotBeWrittenIsSaidAndTriedAgain(t *testing.T) { reader := &fakeReader{st: seatlist.Status{Heard: false}} clock := &fakeClock{now: time.Date(2026, 11, 5, 12, 0, 0, 0, time.UTC)} findings := &fakeFindings{err: errors.New("bus down")} var said []string w := &Waiter{Node: "ace", Observer: "node-engine", Reader: reader, Findings: findings, Clock: clock, Say: func(s string) { said = append(said, s) }} w.Changed() clock.advance(Bound) if len(said) != 1 || len(findings.put) != 0 { t.Fatalf("said %v, wrote %v", said, findings.put) } findings.err = nil w.Changed() clock.advance(Bound) if len(findings.put) != 1 { t.Fatalf("the finding was not written at the next bound: %v", findings.put) } } // ADR 0636 §1, the node's finding of a seat list unread. func TestTheSeatListWait(t *testing.T) { for name, run := range map[string]func(*testing.T){ "five minutes write one finding and the first full read clears it": caseTheNodeWritesOneFindingAfterFiveMinutesAndClearsItAtTheFirstFullRead, "a read within the bound writes nothing": caseAReadWithinTheBoundWritesNothing, "a finding that cannot be written is said and tried again": caseAFindingThatCannotBeWrittenIsSaidAndTriedAgain, } { t.Run(name, run) } }