Files
kyleandClaude Fable 5.1 74b8af4ce0 AGENTS.md: a refused tool call is not a reason to end the turn; v2 plan (props, ctx guard, wake, identity, wiring) as acceptance tests; v1.1 run note
v2 given tests compiled against a panic-only skeleton (go vet clean); no reference
implementation.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-25 08:37:38 -07:00

115 lines
3.8 KiB
Go

package wake_test
import (
"bytes"
"context"
"net"
"testing"
"time"
"git.wntrmute.dev/kyle/crossbar/internal/wake"
)
func listen(t *testing.T) (*net.UDPConn, string) {
conn, err := net.ListenUDP("udp4", &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1)})
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { conn.Close() })
return conn, conn.LocalAddr().String()
}
func TestMagicPacket(t *testing.T) {
pkt, err := wake.MagicPacket("aa:bb:cc:dd:ee:ff")
if err != nil {
t.Fatal(err)
}
if len(pkt) != 102 || !bytes.Equal(pkt[:6], bytes.Repeat([]byte{0xff}, 6)) {
t.Fatalf("packet = % x", pkt)
}
mac := []byte{0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff}
for i := 0; i < 16; i++ {
if !bytes.Equal(pkt[6+6*i:12+6*i], mac) {
t.Fatalf("repetition %d wrong: % x", i, pkt[6+6*i:12+6*i])
}
}
for _, bad := range []string{"", "aa:bb", "zz:bb:cc:dd:ee:ff", "aabbccddeeff00"} {
if _, err := wake.MagicPacket(bad); err == nil {
t.Errorf("MagicPacket(%q) must fail", bad)
}
}
if p2, _ := wake.MagicPacket("AA-BB-CC-DD-EE-FF"); !bytes.Equal(p2, pkt) {
t.Errorf("dash-separated upper-case MAC must give the same packet")
}
}
func TestSendReachesTheBroadcastAddress(t *testing.T) {
conn, addr := listen(t)
if err := wake.Send("aa:bb:cc:dd:ee:ff", addr); err != nil {
t.Fatal(err)
}
buf := make([]byte, 200)
_ = conn.SetReadDeadline(time.Now().Add(time.Second))
n, _, err := conn.ReadFromUDP(buf)
if err != nil || n != 102 {
t.Fatalf("received %d bytes, err %v", n, err)
}
if err := wake.Send("aa:bb:cc:dd:ee:ff", "256.1.1.1:9"); err == nil {
t.Error("an unresolvable broadcast address must be an error")
}
}
// fakeHealth flips to healthy after `after` calls to Healthy.
type fakeHealth struct{ calls, after int }
func (f *fakeHealth) Healthy(name string) bool { f.calls++; return f.calls > f.after }
func TestWakerSendsOncePerWindowAndWaitsForHealth(t *testing.T) {
conn, addr := listen(t)
h := &fakeHealth{after: 3}
w := wake.New(map[string]wake.Target{"titan": {MAC: "aa:bb:cc:dd:ee:ff", Broadcast: addr, Wait: 2 * time.Second}}, h)
w.PollEvery(20 * time.Millisecond) // test hook: how often Wake re-checks health
start := time.Now()
ok := w.Wake(context.Background(), "titan")
if !ok {
t.Fatal("Wake must return true once the host reports healthy")
}
if time.Since(start) > time.Second {
t.Errorf("Wake waited %v for a host that came up after 3 checks", time.Since(start))
}
_ = conn.SetReadDeadline(time.Now().Add(200 * time.Millisecond))
buf := make([]byte, 200)
if n, _, err := conn.ReadFromUDP(buf); err != nil || n != 102 {
t.Fatalf("no magic packet received: %d %v", n, err)
}
// A second Wake inside the same window does not send again (the host is booting).
_ = w.Wake(context.Background(), "titan")
_ = conn.SetReadDeadline(time.Now().Add(150 * time.Millisecond))
if n, _, err := conn.ReadFromUDP(buf); err == nil {
t.Errorf("a second packet (%d bytes) was sent inside the wait window", n)
}
if w.Wake(context.Background(), "nobody") {
t.Errorf("unknown host: Wake must return false")
}
}
func TestWakeGivesUpAfterWait(t *testing.T) {
_, addr := listen(t)
h := &fakeHealth{after: 1 << 30}
w := wake.New(map[string]wake.Target{"titan": {MAC: "aa:bb:cc:dd:ee:ff", Broadcast: addr, Wait: 300 * time.Millisecond}}, h)
w.PollEvery(20 * time.Millisecond)
start := time.Now()
if w.Wake(context.Background(), "titan") {
t.Fatal("Wake must return false when the host never comes up")
}
if d := time.Since(start); d < 250*time.Millisecond || d > 900*time.Millisecond {
t.Errorf("Wake returned after %v, want about the 300ms wait", d)
}
ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
defer cancel()
start = time.Now()
if w.Wake(ctx, "titan") || time.Since(start) > 200*time.Millisecond {
t.Errorf("a cancelled context must end the wait early (took %v)", time.Since(start))
}
}