package hostfiles import ( "context" "path/filepath" "testing" "github.com/carlosframework/platform/internal/registry" "github.com/carlosframework/platform/internal/release" ) func wakeReg(t *testing.T) *registry.Registry { t.Helper() r, err := registry.Open(filepath.Join(t.TempDir(), "registry.db")) if err != nil { t.Fatal(err) } t.Cleanup(func() { r.Close() }) return r } func seedHost(t *testing.T, r *registry.Registry, host string) { t.Helper() if err := r.Upsert(registry.Route{Host: host, Kind: "service", Addr: "10.0.0.1:3000", Status: "ready", Hibernate: true}); err != nil { t.Fatal(err) } } func TestWakeStateRoundTripsThroughTheManifest(t *testing.T) { ctx, s := context.Background(), release.NewDirStore(t.TempDir()) src, dst := wakeReg(t), wakeReg(t) seedHost(t, src, "shop.test") seedHost(t, dst, "shop.test") if err := src.ReplaceTimers("shop.test", []registry.Timer{ {Host: "shop.test", Name: "nightly", Kind: "schedule", Spec: "0 3 * * *", Path: "/n", NextAt: 5000, LastAt: 4000, LastStatus: "ok", Attempts: 1, CreatedAt: 10, DueAt: 4900, Registration: "reg-n"}, {Host: "shop.test", Name: "remind", Kind: "once", Spec: "2026-10-06T00:00:00Z", Path: "/r", NextAt: 6000, CreatedAt: 11, DueAt: 6000, Registration: "reg-r"}, {Host: "shop.test", Name: "done", Kind: "once", Spec: "2026-10-01T00:00:00Z", Path: "/d", NextAt: 0, LastAt: 3000, CreatedAt: 9, Registration: "reg-d"}, }); err != nil { t.Fatal(err) } if err := src.SetNextDue("shop.test", 7777); err != nil { t.Fatal(err) } if err := src.SetReplication("shop.test", registry.ReplicationBatched); err != nil { t.Fatal(err) } ws, err := ExportWake(src, "shop.test") if err != nil { t.Fatal(err) } if len(ws.Timers) != 2 { t.Fatalf("exported %d timers, want 2: a finished once-timer is history, not wake state", len(ws.Timers)) } p := open(t, s) if err := StageWake(ctx, p, ws); err != nil { t.Fatal(err) } commit(t, p) m, _, err := Load(ctx, s, pfx) if err != nil { t.Fatal(err) } got, found, err := LoadWake(ctx, s, pfx, m, nil) if err != nil || !found { t.Fatalf("LoadWake found=%v err=%v", found, err) } if err := ImportWake(dst, "shop.test", got); err != nil { t.Fatal(err) } rows, _ := dst.TimersFor("shop.test") if len(rows) != 2 || rows[0].Name != "nightly" || rows[0].Registration != "reg-n" || rows[0].NextAt != 5000 || rows[0].Attempts != 1 { t.Fatalf("imported timers: %+v", rows) } rt, err := dst.Get("shop.test") if err != nil || rt == nil { t.Fatalf("route: %v %v", rt, err) } if rt.NextDue != 7777 || rt.Replication != registry.ReplicationBatched { t.Fatalf("NextDue=%d Replication=%q", rt.NextDue, rt.Replication) } } func TestStageWakeOfUnchangedStateStagesNothing(t *testing.T) { ctx, s := context.Background(), release.NewDirStore(t.TempDir()) p := open(t, s) ws := WakeState{V: 1, Host: "shop.test", NextDue: 5} if err := StageWake(ctx, p, ws); err != nil { t.Fatal(err) } commit(t, p) if err := StageWake(ctx, p, ws); err != nil { t.Fatal(err) } if p.Dirty() { t.Fatal("unchanged wake state was staged again") } } func TestLoadWakeWithNoneReportsNotFound(t *testing.T) { ctx, s := context.Background(), release.NewDirStore(t.TempDir()) m, _, _ := Load(ctx, s, pfx) if _, found, err := LoadWake(ctx, s, pfx, m, nil); found || err != nil { t.Fatalf("found=%v err=%v", found, err) } } // A manifest that names wake state whose object has gone is lost data, // not a host without timers. func TestLoadWakeWithItsObjectMissingIsAnError(t *testing.T) { ctx, s := context.Background(), release.NewDirStore(t.TempDir()) p := open(t, s) if err := StageWake(ctx, p, WakeState{V: 1, Host: "shop.test", NextDue: 5}); err != nil { t.Fatal(err) } commit(t, p) m, _, err := Load(ctx, s, pfx) if err != nil { t.Fatal(err) } if err := s.Delete(ctx, m.Platform[WakeItem].Object); err != nil { t.Fatal(err) } if _, found, err := LoadWake(ctx, s, pfx, m, nil); err == nil || found { t.Fatalf("found = %v, err = %v; a missing wake object must be an error", found, err) } } func TestImportWakeRefusesAnotherHostsState(t *testing.T) { r := wakeReg(t) seedHost(t, r, "shop.test") if err := ImportWake(r, "shop.test", WakeState{V: 1, Host: "other.test"}); err == nil { t.Fatal("imported another host's wake state") } } func TestImportWakeRefusesAnUnknownReplicationClassAndChangesNothing(t *testing.T) { r := wakeReg(t) seedHost(t, r, "shop.test") if err := r.ReplaceTimers("shop.test", []registry.Timer{{Host: "shop.test", Name: "keep", Kind: "schedule", Spec: "@every 1m", Path: "/k", NextAt: 1, CreatedAt: 1}}); err != nil { t.Fatal(err) } err := ImportWake(r, "shop.test", WakeState{V: 1, Host: "shop.test", Replication: "hourly"}) if err == nil { t.Fatal("accepted replication class \"hourly\"") } if rows, _ := r.TimersFor("shop.test"); len(rows) != 1 { t.Fatalf("a refused import changed the timers: %+v", rows) } } func TestExportWakeOfAnUnknownHostIsAnError(t *testing.T) { if _, err := ExportWake(wakeReg(t), "nobody.test"); err == nil { t.Fatal("exported wake state for a host with no route") } } func TestImportWakeOfTheDefaultClassReplacesAnotherClass(t *testing.T) { r := wakeReg(t) seedHost(t, r, "shop.test") if err := r.SetReplication("shop.test", registry.ReplicationDaily); err != nil { t.Fatal(err) } if err := ImportWake(r, "shop.test", WakeState{V: 1, Host: "shop.test", Replication: ""}); err != nil { t.Fatal(err) } rt, err := r.Get("shop.test") if err != nil || rt == nil { t.Fatalf("route: %v %v", rt, err) } if rt.Replication != "" { t.Fatalf("Replication = %q, want the default", rt.Replication) } } func TestImportWakeForAHostWithNoRouteWritesNothing(t *testing.T) { r := wakeReg(t) if err := r.ReplaceTimers("ghost.test", []registry.Timer{{Host: "ghost.test", Name: "keep", Kind: "schedule", Spec: "@every 1m", Path: "/k", NextAt: 1, CreatedAt: 1}}); err != nil { t.Fatal(err) } ws := WakeState{V: 1, Host: "ghost.test", Timers: []WakeTimer{{Name: "new", Kind: "schedule", Spec: "@every 2m", Path: "/n", NextAt: 2, CreatedAt: 2}}} if err := ImportWake(r, "ghost.test", ws); err == nil { t.Fatal("imported wake state for a host with no route") } rows, _ := r.TimersFor("ghost.test") if len(rows) != 1 || rows[0].Name != "keep" { t.Fatalf("a refused import changed the timers: %+v", rows) } }