agi_scheduler_test.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327
  1. package agi
  2. import (
  3. "errors"
  4. "testing"
  5. "github.com/robertkrimen/otto"
  6. "imuslab.com/arozos/mod/agi/static"
  7. user "imuslab.com/arozos/mod/user"
  8. )
  9. // ─── helpers ─────────────────────────────────────────────────────────────────
  10. // minimalGateway returns a Gateway with only the fields needed by the scheduler tests.
  11. func minimalGateway() *Gateway {
  12. return &Gateway{
  13. ReservedTables: []string{},
  14. NightlyScripts: []string{},
  15. LoadedAGILibrary: map[string]AgiLibInjectionIntergface{},
  16. Option: &AgiSysInfo{},
  17. }
  18. }
  19. // stubUser returns a bare *user.User with just a Username set.
  20. // It does not require a full UserHandler or DB.
  21. func stubUser(name string) *user.User {
  22. return &user.User{Username: name}
  23. }
  24. // stubCallbacks returns a SchedulerCallbacks with all fields populated by
  25. // simple in-memory state, so we can assert calls were made.
  26. func stubCallbacks(registered map[string]bool, canCreate bool) *SchedulerCallbacks {
  27. return &SchedulerCallbacks{
  28. RegisterJob: func(creator, appName, taskName, scriptVpath, fshID, description string, interval, baseTime int64) error {
  29. key := appName + "/" + creator + "/" + taskName
  30. if registered[key] {
  31. return errors.New("already registered")
  32. }
  33. registered[key] = true
  34. return nil
  35. },
  36. UnregisterJob: func(creator, taskName string) error {
  37. for k := range registered {
  38. if k[len(k)-len(taskName):] == taskName {
  39. delete(registered, k)
  40. return nil
  41. }
  42. }
  43. return errors.New("job not found")
  44. },
  45. AppJobExists: func(appName, creator, taskName string) bool {
  46. return registered[appName+"/"+creator+"/"+taskName]
  47. },
  48. CanCreate: func(username string) bool {
  49. return canCreate
  50. },
  51. }
  52. }
  53. // ─── RegisterSchedulerLib ────────────────────────────────────────────────────
  54. func TestRegisterSchedulerLib_AddsToLoadedLibs(t *testing.T) {
  55. g := minimalGateway()
  56. cb := &SchedulerCallbacks{}
  57. g.RegisterSchedulerLib(cb)
  58. if _, ok := g.LoadedAGILibrary["scheduler"]; !ok {
  59. t.Error("expected 'scheduler' in LoadedAGILibrary after RegisterSchedulerLib")
  60. }
  61. }
  62. func TestRegisterSchedulerLib_IdempotentDoesNotPanic(t *testing.T) {
  63. g := minimalGateway()
  64. cb := &SchedulerCallbacks{}
  65. // Second call should log but not panic
  66. g.RegisterSchedulerLib(cb)
  67. g.RegisterSchedulerLib(cb) // should just log a warning
  68. }
  69. // ─── SchedulerCallbacks nil-safety ───────────────────────────────────────────
  70. func TestInjectSchedulerLib_NilCallbacksNoHasPermission(t *testing.T) {
  71. g := minimalGateway()
  72. vm := otto.New()
  73. payload := &static.AgiLibInjectionPayload{
  74. VM: vm,
  75. User: stubUser("alice"),
  76. }
  77. // Should not panic when callbacks is nil
  78. g.injectSchedulerLibFunctions(payload, nil)
  79. val, err := vm.Run(`_scheduler_hasPermission()`)
  80. if err != nil {
  81. t.Fatalf("hasPermission() errored: %v", err)
  82. }
  83. b, _ := val.ToBoolean()
  84. if b {
  85. t.Error("expected false for nil callbacks, got true")
  86. }
  87. }
  88. func TestInjectSchedulerLib_NilCallbacksNoRegistered(t *testing.T) {
  89. g := minimalGateway()
  90. vm := otto.New()
  91. payload := &static.AgiLibInjectionPayload{
  92. VM: vm,
  93. User: stubUser("alice"),
  94. }
  95. g.injectSchedulerLibFunctions(payload, nil)
  96. val, err := vm.Run(`_scheduler_registered("MyTask", "MyApp")`)
  97. if err != nil {
  98. t.Fatalf("registered() errored: %v", err)
  99. }
  100. b, _ := val.ToBoolean()
  101. if b {
  102. t.Error("expected false for nil callbacks, got true")
  103. }
  104. }
  105. // ─── hasPermission ───────────────────────────────────────────────────────────
  106. func TestInjectSchedulerLib_HasPermissionTrue(t *testing.T) {
  107. g := minimalGateway()
  108. vm := otto.New()
  109. cb := stubCallbacks(map[string]bool{}, true)
  110. payload := &static.AgiLibInjectionPayload{VM: vm, User: stubUser("alice")}
  111. g.injectSchedulerLibFunctions(payload, cb)
  112. val, _ := vm.Run(`_scheduler_hasPermission()`)
  113. b, _ := val.ToBoolean()
  114. if !b {
  115. t.Error("expected hasPermission() true for user with permission")
  116. }
  117. }
  118. func TestInjectSchedulerLib_HasPermissionFalse(t *testing.T) {
  119. g := minimalGateway()
  120. vm := otto.New()
  121. cb := stubCallbacks(map[string]bool{}, false)
  122. payload := &static.AgiLibInjectionPayload{VM: vm, User: stubUser("bob")}
  123. g.injectSchedulerLibFunctions(payload, cb)
  124. val, _ := vm.Run(`_scheduler_hasPermission()`)
  125. b, _ := val.ToBoolean()
  126. if b {
  127. t.Error("expected hasPermission() false for user without permission")
  128. }
  129. }
  130. // ─── registered ──────────────────────────────────────────────────────────────
  131. func TestInjectSchedulerLib_RegisteredFalseInitially(t *testing.T) {
  132. g := minimalGateway()
  133. vm := otto.New()
  134. jobs := map[string]bool{}
  135. cb := stubCallbacks(jobs, true)
  136. payload := &static.AgiLibInjectionPayload{VM: vm, User: stubUser("alice")}
  137. g.injectSchedulerLibFunctions(payload, cb)
  138. val, _ := vm.Run(`_scheduler_registered("MyTask", "MyApp")`)
  139. b, _ := val.ToBoolean()
  140. if b {
  141. t.Error("expected registered() false before any registration")
  142. }
  143. }
  144. func TestInjectSchedulerLib_RegisteredTrueAfterRegister(t *testing.T) {
  145. g := minimalGateway()
  146. vm := otto.New()
  147. jobs := map[string]bool{}
  148. cb := stubCallbacks(jobs, true)
  149. payload := &static.AgiLibInjectionPayload{VM: vm, User: stubUser("alice")}
  150. g.injectSchedulerLibFunctions(payload, cb)
  151. // Manually mark as registered in the stub
  152. jobs["MyApp/alice/MyTask"] = true
  153. val, _ := vm.Run(`_scheduler_registered("MyTask", "MyApp")`)
  154. b, _ := val.ToBoolean()
  155. if !b {
  156. t.Error("expected registered() true after job was added to the stub")
  157. }
  158. }
  159. // ─── register ────────────────────────────────────────────────────────────────
  160. func TestInjectSchedulerLib_RegisterReturnsTrue(t *testing.T) {
  161. g := minimalGateway()
  162. vm := otto.New()
  163. jobs := map[string]bool{}
  164. cb := stubCallbacks(jobs, true)
  165. payload := &static.AgiLibInjectionPayload{VM: vm, User: stubUser("alice")}
  166. g.injectSchedulerLibFunctions(payload, cb)
  167. _, err := vm.Run(`var ok = _scheduler_register("DailyTask", "MyApp", 86400, "desc", "cron.agi");`)
  168. if err != nil {
  169. t.Fatalf("register() error: %v", err)
  170. }
  171. val, _ := vm.Get("ok")
  172. b, _ := val.ToBoolean()
  173. if !b {
  174. t.Error("expected register() to return true on first registration")
  175. }
  176. if !jobs["MyApp/alice/DailyTask"] {
  177. t.Error("expected stub jobs map to contain MyApp/alice/DailyTask")
  178. }
  179. }
  180. func TestInjectSchedulerLib_RegisterReturnsFalseOnDuplicate(t *testing.T) {
  181. g := minimalGateway()
  182. vm := otto.New()
  183. jobs := map[string]bool{}
  184. cb := stubCallbacks(jobs, true)
  185. payload := &static.AgiLibInjectionPayload{VM: vm, User: stubUser("alice")}
  186. g.injectSchedulerLibFunctions(payload, cb)
  187. vm.Run(`_scheduler_register("DailyTask", "MyApp", 86400, "desc", "cron.agi")`)
  188. _, err := vm.Run(`var ok2 = _scheduler_register("DailyTask", "MyApp", 86400, "desc", "cron.agi");`)
  189. if err != nil {
  190. // otto may surface a raised error — acceptable; the return value test is redundant
  191. return
  192. }
  193. val, _ := vm.Get("ok2")
  194. b, _ := val.ToBoolean()
  195. if b {
  196. t.Error("expected register() to return false on duplicate registration")
  197. }
  198. }
  199. func TestInjectSchedulerLib_RegisterMissingAppNameReturnsFalse(t *testing.T) {
  200. g := minimalGateway()
  201. vm := otto.New()
  202. cb := stubCallbacks(map[string]bool{}, true)
  203. payload := &static.AgiLibInjectionPayload{VM: vm, User: stubUser("alice")}
  204. g.injectSchedulerLibFunctions(payload, cb)
  205. _, err := vm.Run(`var ok = _scheduler_register("task", "", 60, "", "cron.agi");`)
  206. if err != nil {
  207. return // raised error is also acceptable
  208. }
  209. val, _ := vm.Get("ok")
  210. b, _ := val.ToBoolean()
  211. if b {
  212. t.Error("expected register() to return false when appName is empty")
  213. }
  214. }
  215. // ─── unregister ──────────────────────────────────────────────────────────────
  216. func TestInjectSchedulerLib_UnregisterRemovesJob(t *testing.T) {
  217. g := minimalGateway()
  218. vm := otto.New()
  219. jobs := map[string]bool{"MyApp/alice/DailyTask": true}
  220. cb := stubCallbacks(jobs, true)
  221. payload := &static.AgiLibInjectionPayload{VM: vm, User: stubUser("alice")}
  222. g.injectSchedulerLibFunctions(payload, cb)
  223. _, err := vm.Run(`var ok = _scheduler_unregister("DailyTask");`)
  224. if err != nil {
  225. t.Fatalf("unregister() error: %v", err)
  226. }
  227. val, _ := vm.Get("ok")
  228. b, _ := val.ToBoolean()
  229. if !b {
  230. t.Error("expected unregister() to return true for existing job")
  231. }
  232. if jobs["MyApp/alice/DailyTask"] {
  233. t.Error("expected job to be removed from stub after unregister")
  234. }
  235. }
  236. func TestInjectSchedulerLib_UnregisterNonExistentReturnsFalse(t *testing.T) {
  237. g := minimalGateway()
  238. vm := otto.New()
  239. cb := stubCallbacks(map[string]bool{}, true)
  240. payload := &static.AgiLibInjectionPayload{VM: vm, User: stubUser("alice")}
  241. g.injectSchedulerLibFunctions(payload, cb)
  242. _, err := vm.Run(`var ok = _scheduler_unregister("Ghost");`)
  243. if err != nil {
  244. return // raised error acceptable
  245. }
  246. val, _ := vm.Get("ok")
  247. b, _ := val.ToBoolean()
  248. if b {
  249. t.Error("expected unregister() to return false for non-existent job")
  250. }
  251. }
  252. // ─── JS scheduler object ─────────────────────────────────────────────────────
  253. func TestInjectSchedulerLib_JSObjectExposed(t *testing.T) {
  254. g := minimalGateway()
  255. vm := otto.New()
  256. cb := stubCallbacks(map[string]bool{}, true)
  257. payload := &static.AgiLibInjectionPayload{VM: vm, User: stubUser("alice")}
  258. g.injectSchedulerLibFunctions(payload, cb)
  259. for _, method := range []string{"hasPermission", "registered", "register", "unregister"} {
  260. val, err := vm.Run(`typeof scheduler.` + method)
  261. if err != nil {
  262. t.Fatalf("evaluating scheduler.%s: %v", method, err)
  263. }
  264. s, _ := val.ToString()
  265. if s != "function" {
  266. t.Errorf("scheduler.%s should be a function, got %q", method, s)
  267. }
  268. }
  269. }
  270. func TestInjectSchedulerLib_JSObjectHasPermission(t *testing.T) {
  271. g := minimalGateway()
  272. vm := otto.New()
  273. cb := stubCallbacks(map[string]bool{}, true)
  274. payload := &static.AgiLibInjectionPayload{VM: vm, User: stubUser("alice")}
  275. g.injectSchedulerLibFunctions(payload, cb)
  276. val, err := vm.Run(`scheduler.hasPermission()`)
  277. if err != nil {
  278. t.Fatalf("scheduler.hasPermission(): %v", err)
  279. }
  280. b, _ := val.ToBoolean()
  281. if !b {
  282. t.Error("scheduler.hasPermission() should return true for permitted user")
  283. }
  284. }