1package nightsky23import (4 "testing"5 "time"67 "gno.land/p/nt/avl/v0"8 "gno.land/p/nt/uassert/v0"9 "gno.land/p/nt/urequire/v0"10)1112const (13 owner = "g1owner"14 guest = "g1guest"15)1617func newTestRealm() *TelescopeRealm {18 cfg := NewTelescopeConfig("Test Scope", "Seestar S50", 49.0, 1.0, owner, "/r/test/telescope")19 return NewTelescopeRealm(&cfg)20}2122func TestNewTelescopeConfig(t *testing.T) {23 cfg := NewTelescopeConfig("Scope", "Model", 1.5, 2.5, owner, "/r/x/y")24 uassert.Equal(t, "Scope", cfg.Name)25 uassert.Equal(t, "offline", cfg.Status) // starts offline26 uassert.Equal(t, owner, cfg.Owner)2728 cfg.UpdateStatus("online")29 uassert.Equal(t, "online", cfg.Status)30}3132func TestValidateCommand(cur realm, t *testing.T) {33 cfg := NewTelescopeConfig("Scope", "Model", 0, 0, owner, "/r/x/y")3435 // Valid capture and stop should not panic.36 uassert.NotPanics(t, cur, func() {37 ValidateCommand(TelescopeCommand{CommandType: "capture", TargetRA: 5.5, TargetDec: 22.5, Exposure: 60}, cfg)38 })39 uassert.NotPanics(t, cur, func() {40 // stop ignores coordinates entirely.41 ValidateCommand(TelescopeCommand{CommandType: "stop", TargetRA: 999, TargetDec: 999, Exposure: 0}, cfg)42 })4344 cases := []struct {45 name string46 cmd TelescopeCommand47 msg string48 }{49 {"bad type", TelescopeCommand{CommandType: "explode"}, "invalid command type"},50 {"RA low", TelescopeCommand{CommandType: "capture", TargetRA: -1, Exposure: 10}, "invalid RA"},51 {"RA high", TelescopeCommand{CommandType: "capture", TargetRA: 24, Exposure: 10}, "invalid RA"},52 {"Dec low", TelescopeCommand{CommandType: "capture", TargetRA: 1, TargetDec: -91, Exposure: 10}, "invalid Dec"},53 {"Dec high", TelescopeCommand{CommandType: "capture", TargetRA: 1, TargetDec: 91, Exposure: 10}, "invalid Dec"},54 {"exp low", TelescopeCommand{CommandType: "capture", TargetRA: 1, Exposure: 0}, "invalid exposure"},55 {"exp high", TelescopeCommand{CommandType: "capture", TargetRA: 1, Exposure: 301}, "invalid exposure"},56 }57 for _, tc := range cases {58 cmd := tc.cmd59 uassert.PanicsContains(t, cur, tc.msg, func() { ValidateCommand(cmd, cfg) }, tc.name)60 }61}6263func TestIsAccessAllowed(t *testing.T) {64 rules := avl.NewTree()6566 // Unknown address: denied.67 uassert.False(t, IsAccessAllowed(guest, rules))6869 // Never-expires rule (zero time): allowed.70 rules.Set(guest, AccessRule{AllowedAddress: guest, ExpiresAt: time.Time{}})71 uassert.True(t, IsAccessAllowed(guest, rules))7273 // Future expiry: allowed.74 rules.Set(guest, AccessRule{AllowedAddress: guest, ExpiresAt: time.Now().Add(time.Hour)})75 uassert.True(t, IsAccessAllowed(guest, rules))7677 // Past expiry: denied.78 rules.Set(guest, AccessRule{AllowedAddress: guest, ExpiresAt: time.Now().Add(-time.Hour)})79 uassert.False(t, IsAccessAllowed(guest, rules))80}8182func TestAccessFlow(cur realm, t *testing.T) {83 r := newTestRealm()8485 // Owner can always submit.86 urequire.NotPanics(t, cur, func() {87 r.SubmitCommand(owner, "capture", 5, 10, 30)88 })8990 // Guest without a grant is denied.91 uassert.PanicsContains(t, cur, "access denied", func() {92 r.SubmitCommand(guest, "capture", 5, 10, 30)93 })9495 // Grant, then guest can submit.96 r.GrantAccess(owner, guest, 0) // 0 = never expires97 uassert.Equal(t, 1, r.GetAccessRuleCount())98 urequire.NotPanics(t, cur, func() {99 r.SubmitCommand(guest, "capture", 5, 10, 30)100 })101102 // Re-granting the same address does not duplicate the rule.103 r.GrantAccess(owner, guest, 5)104 uassert.Equal(t, 1, r.GetAccessRuleCount())105106 // Revoke, then guest is denied again.107 r.RevokeAccess(owner, guest)108 uassert.Equal(t, 0, r.GetAccessRuleCount())109 uassert.PanicsContains(t, cur, "access denied", func() {110 r.SubmitCommand(guest, "capture", 5, 10, 30)111 })112113 // Non-owner cannot grant access.114 uassert.PanicsContains(t, cur, "owner only", func() {115 r.GrantAccess(guest, "g1evil", 0)116 })117}118119func TestCommandQueueFIFO(cur realm, t *testing.T) {120 r := newTestRealm()121122 r.SubmitCommand(owner, "capture", 1, 1, 10)123 r.SubmitCommand(owner, "capture", 2, 2, 20)124 r.SubmitCommand(owner, "stop", 0, 0, 0)125 uassert.Equal(t, 3, r.GetCommandCount())126127 // FIFO order.128 c1 := r.GetNextCommand(owner)129 uassert.Equal(t, "capture", c1.CommandType)130 uassert.Equal(t, int64(10), int64(c1.Exposure))131132 c2 := r.GetNextCommand(owner)133 uassert.Equal(t, int64(20), int64(c2.Exposure))134135 c3 := r.GetNextCommand(owner)136 uassert.Equal(t, "stop", c3.CommandType)137138 uassert.Equal(t, 0, r.GetCommandCount())139140 // Draining an empty queue panics.141 uassert.PanicsContains(t, cur, "no commands", func() { r.GetNextCommand(owner) })142143 // Only the owner may pull commands.144 r.SubmitCommand(owner, "stop", 0, 0, 0)145 uassert.PanicsContains(t, cur, "owner only", func() { r.GetNextCommand(guest) })146147 // Emergency stop clears everything and sets status online.148 r.ClearCommandQueue(owner)149 uassert.Equal(t, 0, r.GetCommandCount())150 uassert.Equal(t, "online", r.Config.Status)151}152153func TestRecordCaptureUpdatesStatus(cur realm, t *testing.T) {154 r := newTestRealm()155156 // Submitting a command flips status to busy.157 r.SubmitCommand(owner, "capture", 5, 10, 30)158 uassert.Equal(t, "busy", r.Config.Status)159160 // Recording a capture flips it back to online and stores the capture.161 r.RecordCapture(owner, "https://img/1.png", 5, 10, 30, owner)162 uassert.Equal(t, "online", r.Config.Status)163 uassert.Equal(t, 1, r.GetCaptureCount())164165 // Empty image URL is rejected.166 uassert.PanicsContains(t, cur, "image URL", func() {167 r.RecordCapture(owner, "", 5, 10, 30, owner)168 })169170 // Non-owner cannot record.171 uassert.PanicsContains(t, cur, "owner only", func() {172 r.RecordCapture(guest, "https://img/2.png", 5, 10, 30, guest)173 })174}175176// --- CaptureFeed tests ------------------------------------------------------177178func mkCap(url string) CaptureResult {179 return CaptureResult{ImageURL: url}180}181182func TestCaptureFeedOrderAndLatest(t *testing.T) {183 f := NewCaptureFeed(10)184 uassert.Equal(t, 0, f.Len())185186 _, ok := f.Latest()187 uassert.False(t, ok)188189 f.Add(mkCap("a"))190 f.Add(mkCap("b"))191 f.Add(mkCap("c"))192 uassert.Equal(t, 3, f.Len())193194 latest, ok := f.Latest()195 urequire.True(t, ok)196 uassert.Equal(t, "c", latest.ImageURL)197198 // Newest-first iteration order.199 var got []string200 f.IterateNewest(func(c CaptureResult) bool {201 got = append(got, c.ImageURL)202 return false203 })204 urequire.Equal(t, 3, len(got))205 uassert.Equal(t, "c", got[0])206 uassert.Equal(t, "b", got[1])207 uassert.Equal(t, "a", got[2])208209 // Early stop.210 count := 0211 f.IterateNewest(func(c CaptureResult) bool {212 count++213 return true // stop after first214 })215 uassert.Equal(t, 1, count)216}217218func TestCaptureFeedTrim(t *testing.T) {219 f := NewCaptureFeed(3)220 f.Add(mkCap("1"))221 f.Add(mkCap("2"))222 f.Add(mkCap("3"))223 f.Add(mkCap("4")) // evicts "1"224 f.Add(mkCap("5")) // evicts "2"225226 uassert.Equal(t, 3, f.Len())227228 var got []string229 f.IterateNewest(func(c CaptureResult) bool {230 got = append(got, c.ImageURL)231 return false232 })233 // Newest-first: 5, 4, 3 — oldest two evicted.234 uassert.Equal(t, "5", got[0])235 uassert.Equal(t, "4", got[1])236 uassert.Equal(t, "3", got[2])237}238239func TestCaptureFeedRemoveAt(t *testing.T) {240 f := NewCaptureFeed(10)241 f.Add(mkCap("a"))242 f.Add(mkCap("b"))243 f.Add(mkCap("c")) // newest-first: c(0), b(1), a(2)244245 // Out-of-range is a no-op returning false.246 uassert.False(t, f.RemoveAt(-1))247 uassert.False(t, f.RemoveAt(3))248 uassert.Equal(t, 3, f.Len())249250 // Remove the middle (b).251 uassert.True(t, f.RemoveAt(1))252 uassert.Equal(t, 2, f.Len())253254 var got []string255 f.IterateNewest(func(c CaptureResult) bool {256 got = append(got, c.ImageURL)257 return false258 })259 uassert.Equal(t, "c", got[0])260 uassert.Equal(t, "a", got[1])261262 // Removing newest (c).263 uassert.True(t, f.RemoveAt(0))264 latest, ok := f.Latest()265 urequire.True(t, ok)266 uassert.Equal(t, "a", latest.ImageURL)267}268269func TestNewCaptureFeedRejectsNonPositive(cur realm, t *testing.T) {270 uassert.PanicsContains(t, cur, "must be positive", func() { NewCaptureFeed(0) })271}272Signatures reconstructed verbatim from vm/qfuncs — interface params keep their inline definitions.