@rebasepro/server-core 0.0.1-canary.000dc36

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (305) hide show
  1. package/LICENSE +6 -0
  2. package/README.md +40 -0
  3. package/build-errors.txt +52 -0
  4. package/coverage/clover.xml +3739 -0
  5. package/coverage/coverage-final.json +31 -0
  6. package/coverage/lcov-report/base.css +224 -0
  7. package/coverage/lcov-report/block-navigation.js +87 -0
  8. package/coverage/lcov-report/favicon.png +0 -0
  9. package/coverage/lcov-report/index.html +266 -0
  10. package/coverage/lcov-report/prettify.css +1 -0
  11. package/coverage/lcov-report/prettify.js +2 -0
  12. package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
  13. package/coverage/lcov-report/sorter.js +210 -0
  14. package/coverage/lcov-report/src/api/ast-schema-editor.ts.html +952 -0
  15. package/coverage/lcov-report/src/api/errors.ts.html +472 -0
  16. package/coverage/lcov-report/src/api/graphql/graphql-schema-generator.ts.html +1069 -0
  17. package/coverage/lcov-report/src/api/graphql/index.html +116 -0
  18. package/coverage/lcov-report/src/api/index.html +176 -0
  19. package/coverage/lcov-report/src/api/openapi-generator.ts.html +565 -0
  20. package/coverage/lcov-report/src/api/rest/api-generator.ts.html +994 -0
  21. package/coverage/lcov-report/src/api/rest/index.html +131 -0
  22. package/coverage/lcov-report/src/api/rest/query-parser.ts.html +550 -0
  23. package/coverage/lcov-report/src/api/schema-editor-routes.ts.html +202 -0
  24. package/coverage/lcov-report/src/api/server.ts.html +823 -0
  25. package/coverage/lcov-report/src/auth/admin-routes.ts.html +973 -0
  26. package/coverage/lcov-report/src/auth/index.html +176 -0
  27. package/coverage/lcov-report/src/auth/jwt.ts.html +574 -0
  28. package/coverage/lcov-report/src/auth/middleware.ts.html +745 -0
  29. package/coverage/lcov-report/src/auth/password.ts.html +310 -0
  30. package/coverage/lcov-report/src/auth/services.ts.html +2074 -0
  31. package/coverage/lcov-report/src/collections/index.html +116 -0
  32. package/coverage/lcov-report/src/collections/loader.ts.html +232 -0
  33. package/coverage/lcov-report/src/db/auth-schema.ts.html +523 -0
  34. package/coverage/lcov-report/src/db/data-transformer.ts.html +1753 -0
  35. package/coverage/lcov-report/src/db/entityService.ts.html +700 -0
  36. package/coverage/lcov-report/src/db/index.html +146 -0
  37. package/coverage/lcov-report/src/db/services/EntityFetchService.ts.html +4048 -0
  38. package/coverage/lcov-report/src/db/services/EntityPersistService.ts.html +883 -0
  39. package/coverage/lcov-report/src/db/services/RelationService.ts.html +3121 -0
  40. package/coverage/lcov-report/src/db/services/entity-helpers.ts.html +442 -0
  41. package/coverage/lcov-report/src/db/services/index.html +176 -0
  42. package/coverage/lcov-report/src/db/services/index.ts.html +124 -0
  43. package/coverage/lcov-report/src/generate-drizzle-schema-logic.ts.html +1960 -0
  44. package/coverage/lcov-report/src/index.html +116 -0
  45. package/coverage/lcov-report/src/services/driver-registry.ts.html +631 -0
  46. package/coverage/lcov-report/src/services/index.html +131 -0
  47. package/coverage/lcov-report/src/services/postgresDataDriver.ts.html +3025 -0
  48. package/coverage/lcov-report/src/storage/LocalStorageController.ts.html +1189 -0
  49. package/coverage/lcov-report/src/storage/S3StorageController.ts.html +970 -0
  50. package/coverage/lcov-report/src/storage/index.html +161 -0
  51. package/coverage/lcov-report/src/storage/storage-registry.ts.html +646 -0
  52. package/coverage/lcov-report/src/storage/types.ts.html +451 -0
  53. package/coverage/lcov-report/src/utils/drizzle-conditions.ts.html +3082 -0
  54. package/coverage/lcov-report/src/utils/index.html +116 -0
  55. package/coverage/lcov.info +7179 -0
  56. package/dist/common/src/collections/CollectionRegistry.d.ts +56 -0
  57. package/dist/common/src/collections/index.d.ts +1 -0
  58. package/dist/common/src/data/buildRebaseData.d.ts +14 -0
  59. package/dist/common/src/index.d.ts +3 -0
  60. package/dist/common/src/util/builders.d.ts +57 -0
  61. package/dist/common/src/util/callbacks.d.ts +6 -0
  62. package/dist/common/src/util/collections.d.ts +11 -0
  63. package/dist/common/src/util/common.d.ts +2 -0
  64. package/dist/common/src/util/conditions.d.ts +26 -0
  65. package/dist/common/src/util/entities.d.ts +58 -0
  66. package/dist/common/src/util/enums.d.ts +3 -0
  67. package/dist/common/src/util/index.d.ts +16 -0
  68. package/dist/common/src/util/navigation_from_path.d.ts +34 -0
  69. package/dist/common/src/util/navigation_utils.d.ts +20 -0
  70. package/dist/common/src/util/parent_references_from_path.d.ts +6 -0
  71. package/dist/common/src/util/paths.d.ts +14 -0
  72. package/dist/common/src/util/permissions.d.ts +5 -0
  73. package/dist/common/src/util/references.d.ts +2 -0
  74. package/dist/common/src/util/relations.d.ts +22 -0
  75. package/dist/common/src/util/resolutions.d.ts +72 -0
  76. package/dist/common/src/util/storage.d.ts +24 -0
  77. package/dist/index-DXVBFp5V.js +37 -0
  78. package/dist/index-DXVBFp5V.js.map +1 -0
  79. package/dist/index.es.js +49249 -0
  80. package/dist/index.es.js.map +1 -0
  81. package/dist/index.umd.js +49283 -0
  82. package/dist/index.umd.js.map +1 -0
  83. package/dist/server-core/src/api/ast-schema-editor.d.ts +21 -0
  84. package/dist/server-core/src/api/collections_for_test/callbacks_test_collection.d.ts +2 -0
  85. package/dist/server-core/src/api/errors.d.ts +35 -0
  86. package/dist/server-core/src/api/graphql/graphql-schema-generator.d.ts +35 -0
  87. package/dist/server-core/src/api/graphql/index.d.ts +1 -0
  88. package/dist/server-core/src/api/index.d.ts +9 -0
  89. package/dist/server-core/src/api/openapi-generator.d.ts +16 -0
  90. package/dist/server-core/src/api/rest/api-generator.d.ts +76 -0
  91. package/dist/server-core/src/api/rest/index.d.ts +1 -0
  92. package/dist/server-core/src/api/rest/query-parser.d.ts +9 -0
  93. package/dist/server-core/src/api/schema-editor-routes.d.ts +3 -0
  94. package/dist/server-core/src/api/server.d.ts +40 -0
  95. package/dist/server-core/src/api/types.d.ts +90 -0
  96. package/dist/server-core/src/auth/admin-routes.d.ts +21 -0
  97. package/dist/server-core/src/auth/apple-oauth.d.ts +30 -0
  98. package/dist/server-core/src/auth/bitbucket-oauth.d.ts +11 -0
  99. package/dist/server-core/src/auth/discord-oauth.d.ts +14 -0
  100. package/dist/server-core/src/auth/facebook-oauth.d.ts +14 -0
  101. package/dist/server-core/src/auth/github-oauth.d.ts +15 -0
  102. package/dist/server-core/src/auth/gitlab-oauth.d.ts +13 -0
  103. package/dist/server-core/src/auth/google-oauth.d.ts +14 -0
  104. package/dist/server-core/src/auth/index.d.ts +23 -0
  105. package/dist/server-core/src/auth/interfaces.d.ts +309 -0
  106. package/dist/server-core/src/auth/jwt.d.ts +43 -0
  107. package/dist/server-core/src/auth/linkedin-oauth.d.ts +18 -0
  108. package/dist/server-core/src/auth/microsoft-oauth.d.ts +16 -0
  109. package/dist/server-core/src/auth/middleware.d.ts +81 -0
  110. package/dist/server-core/src/auth/password.d.ts +22 -0
  111. package/dist/server-core/src/auth/rate-limiter.d.ts +31 -0
  112. package/dist/server-core/src/auth/routes.d.ts +27 -0
  113. package/dist/server-core/src/auth/slack-oauth.d.ts +12 -0
  114. package/dist/server-core/src/auth/spotify-oauth.d.ts +12 -0
  115. package/dist/server-core/src/auth/twitter-oauth.d.ts +18 -0
  116. package/dist/server-core/src/bootstrappers/index.d.ts +0 -0
  117. package/dist/server-core/src/collections/BackendCollectionRegistry.d.ts +13 -0
  118. package/dist/server-core/src/collections/loader.d.ts +5 -0
  119. package/dist/server-core/src/cron/cron-loader.d.ts +17 -0
  120. package/dist/server-core/src/cron/cron-routes.d.ts +14 -0
  121. package/dist/server-core/src/cron/cron-scheduler.d.ts +106 -0
  122. package/dist/server-core/src/cron/cron-store.d.ts +32 -0
  123. package/dist/server-core/src/cron/index.d.ts +6 -0
  124. package/dist/server-core/src/db/interfaces.d.ts +18 -0
  125. package/dist/server-core/src/email/index.d.ts +6 -0
  126. package/dist/server-core/src/email/smtp-email-service.d.ts +25 -0
  127. package/dist/server-core/src/email/templates.d.ts +42 -0
  128. package/dist/server-core/src/email/types.d.ts +107 -0
  129. package/dist/server-core/src/functions/function-loader.d.ts +17 -0
  130. package/dist/server-core/src/functions/function-routes.d.ts +10 -0
  131. package/dist/server-core/src/functions/index.d.ts +3 -0
  132. package/dist/server-core/src/history/history-routes.d.ts +23 -0
  133. package/dist/server-core/src/history/index.d.ts +1 -0
  134. package/dist/server-core/src/index.d.ts +29 -0
  135. package/dist/server-core/src/init.d.ts +168 -0
  136. package/dist/server-core/src/serve-spa.d.ts +30 -0
  137. package/dist/server-core/src/services/driver-registry.d.ts +78 -0
  138. package/dist/server-core/src/singleton.d.ts +35 -0
  139. package/dist/server-core/src/storage/LocalStorageController.d.ts +46 -0
  140. package/dist/server-core/src/storage/S3StorageController.d.ts +36 -0
  141. package/dist/server-core/src/storage/index.d.ts +25 -0
  142. package/dist/server-core/src/storage/routes.d.ts +38 -0
  143. package/dist/server-core/src/storage/storage-registry.d.ts +78 -0
  144. package/dist/server-core/src/storage/types.d.ts +103 -0
  145. package/dist/server-core/src/types/index.d.ts +11 -0
  146. package/dist/server-core/src/utils/dev-port.d.ts +35 -0
  147. package/dist/server-core/src/utils/logger.d.ts +31 -0
  148. package/dist/server-core/src/utils/logging.d.ts +9 -0
  149. package/dist/server-core/src/utils/request-logger.d.ts +19 -0
  150. package/dist/server-core/src/utils/sql.d.ts +27 -0
  151. package/dist/types/src/controllers/analytics_controller.d.ts +7 -0
  152. package/dist/types/src/controllers/auth.d.ts +119 -0
  153. package/dist/types/src/controllers/client.d.ts +170 -0
  154. package/dist/types/src/controllers/collection_registry.d.ts +46 -0
  155. package/dist/types/src/controllers/customization_controller.d.ts +60 -0
  156. package/dist/types/src/controllers/data.d.ts +168 -0
  157. package/dist/types/src/controllers/data_driver.d.ts +195 -0
  158. package/dist/types/src/controllers/database_admin.d.ts +11 -0
  159. package/dist/types/src/controllers/dialogs_controller.d.ts +36 -0
  160. package/dist/types/src/controllers/effective_role.d.ts +4 -0
  161. package/dist/types/src/controllers/email.d.ts +34 -0
  162. package/dist/types/src/controllers/index.d.ts +18 -0
  163. package/dist/types/src/controllers/local_config_persistence.d.ts +20 -0
  164. package/dist/types/src/controllers/navigation.d.ts +213 -0
  165. package/dist/types/src/controllers/registry.d.ts +54 -0
  166. package/dist/types/src/controllers/side_dialogs_controller.d.ts +67 -0
  167. package/dist/types/src/controllers/side_entity_controller.d.ts +90 -0
  168. package/dist/types/src/controllers/snackbar.d.ts +24 -0
  169. package/dist/types/src/controllers/storage.d.ts +171 -0
  170. package/dist/types/src/index.d.ts +4 -0
  171. package/dist/types/src/rebase_context.d.ts +105 -0
  172. package/dist/types/src/types/backend.d.ts +536 -0
  173. package/dist/types/src/types/backend_hooks.d.ts +187 -0
  174. package/dist/types/src/types/builders.d.ts +15 -0
  175. package/dist/types/src/types/chips.d.ts +5 -0
  176. package/dist/types/src/types/collections.d.ts +857 -0
  177. package/dist/types/src/types/cron.d.ts +102 -0
  178. package/dist/types/src/types/data_source.d.ts +64 -0
  179. package/dist/types/src/types/entities.d.ts +145 -0
  180. package/dist/types/src/types/entity_actions.d.ts +98 -0
  181. package/dist/types/src/types/entity_callbacks.d.ts +173 -0
  182. package/dist/types/src/types/entity_link_builder.d.ts +7 -0
  183. package/dist/types/src/types/entity_overrides.d.ts +10 -0
  184. package/dist/types/src/types/entity_views.d.ts +59 -0
  185. package/dist/types/src/types/export_import.d.ts +21 -0
  186. package/dist/types/src/types/formex.d.ts +40 -0
  187. package/dist/types/src/types/index.d.ts +25 -0
  188. package/dist/types/src/types/locales.d.ts +4 -0
  189. package/dist/types/src/types/modify_collections.d.ts +5 -0
  190. package/dist/types/src/types/plugins.d.ts +282 -0
  191. package/dist/types/src/types/properties.d.ts +1148 -0
  192. package/dist/types/src/types/property_config.d.ts +70 -0
  193. package/dist/types/src/types/relations.d.ts +336 -0
  194. package/dist/types/src/types/slots.d.ts +262 -0
  195. package/dist/types/src/types/translations.d.ts +874 -0
  196. package/dist/types/src/types/user_management_delegate.d.ts +121 -0
  197. package/dist/types/src/types/websockets.d.ts +78 -0
  198. package/dist/types/src/users/index.d.ts +2 -0
  199. package/dist/types/src/users/roles.d.ts +22 -0
  200. package/dist/types/src/users/user.d.ts +46 -0
  201. package/history_diff.log +385 -0
  202. package/jest.config.cjs +16 -0
  203. package/package.json +86 -0
  204. package/scratch.ts +9 -0
  205. package/src/api/ast-schema-editor.ts +289 -0
  206. package/src/api/collections_for_test/callbacks_test_collection.ts +60 -0
  207. package/src/api/errors.ts +179 -0
  208. package/src/api/graphql/graphql-schema-generator.ts +336 -0
  209. package/src/api/graphql/index.ts +2 -0
  210. package/src/api/index.ts +11 -0
  211. package/src/api/openapi-generator.ts +715 -0
  212. package/src/api/rest/api-generator-count.test.ts +113 -0
  213. package/src/api/rest/api-generator.ts +573 -0
  214. package/src/api/rest/index.ts +2 -0
  215. package/src/api/rest/query-parser.ts +155 -0
  216. package/src/api/schema-editor-routes.ts +41 -0
  217. package/src/api/server.ts +249 -0
  218. package/src/api/types.ts +90 -0
  219. package/src/auth/admin-routes.ts +605 -0
  220. package/src/auth/apple-oauth.ts +120 -0
  221. package/src/auth/bitbucket-oauth.ts +82 -0
  222. package/src/auth/discord-oauth.ts +83 -0
  223. package/src/auth/facebook-oauth.ts +72 -0
  224. package/src/auth/github-oauth.ts +110 -0
  225. package/src/auth/gitlab-oauth.ts +70 -0
  226. package/src/auth/google-oauth.ts +48 -0
  227. package/src/auth/index.ts +34 -0
  228. package/src/auth/interfaces.ts +363 -0
  229. package/src/auth/jwt.ts +181 -0
  230. package/src/auth/linkedin-oauth.ts +81 -0
  231. package/src/auth/microsoft-oauth.ts +88 -0
  232. package/src/auth/middleware.ts +384 -0
  233. package/src/auth/password.ts +77 -0
  234. package/src/auth/rate-limiter.ts +133 -0
  235. package/src/auth/routes.ts +788 -0
  236. package/src/auth/slack-oauth.ts +71 -0
  237. package/src/auth/spotify-oauth.ts +67 -0
  238. package/src/auth/twitter-oauth.ts +120 -0
  239. package/src/bootstrappers/index.ts +1 -0
  240. package/src/collections/BackendCollectionRegistry.ts +20 -0
  241. package/src/collections/loader.ts +49 -0
  242. package/src/cron/cron-loader.ts +89 -0
  243. package/src/cron/cron-routes.test.ts +265 -0
  244. package/src/cron/cron-routes.ts +85 -0
  245. package/src/cron/cron-scheduler.test.ts +547 -0
  246. package/src/cron/cron-scheduler.ts +576 -0
  247. package/src/cron/cron-store.ts +163 -0
  248. package/src/cron/index.ts +6 -0
  249. package/src/db/interfaces.ts +60 -0
  250. package/src/email/index.ts +18 -0
  251. package/src/email/smtp-email-service.ts +91 -0
  252. package/src/email/templates.ts +388 -0
  253. package/src/email/types.ts +105 -0
  254. package/src/functions/function-loader.ts +119 -0
  255. package/src/functions/function-routes.ts +31 -0
  256. package/src/functions/index.ts +3 -0
  257. package/src/history/history-routes.ts +129 -0
  258. package/src/history/index.ts +2 -0
  259. package/src/index.ts +66 -0
  260. package/src/init.ts +737 -0
  261. package/src/serve-spa.ts +81 -0
  262. package/src/services/driver-registry.ts +182 -0
  263. package/src/singleton.test.ts +28 -0
  264. package/src/singleton.ts +70 -0
  265. package/src/storage/LocalStorageController.ts +365 -0
  266. package/src/storage/S3StorageController.ts +298 -0
  267. package/src/storage/index.ts +43 -0
  268. package/src/storage/routes.ts +264 -0
  269. package/src/storage/storage-registry.ts +187 -0
  270. package/src/storage/types.ts +134 -0
  271. package/src/types/index.ts +27 -0
  272. package/src/utils/dev-port.ts +176 -0
  273. package/src/utils/logger.ts +143 -0
  274. package/src/utils/logging.ts +38 -0
  275. package/src/utils/request-logger.ts +66 -0
  276. package/src/utils/sql.ts +38 -0
  277. package/test/admin-routes.test.ts +640 -0
  278. package/test/api-generator.test.ts +501 -0
  279. package/test/ast-schema-editor.test.ts +63 -0
  280. package/test/auth-middleware-hono.test.ts +556 -0
  281. package/test/auth-routes.test.ts +1047 -0
  282. package/test/backend-hooks-admin.test.ts +394 -0
  283. package/test/backend-hooks-data.test.ts +408 -0
  284. package/test/driver-registry.test.ts +282 -0
  285. package/test/error-propagation.test.ts +226 -0
  286. package/test/errors-hono.test.ts +133 -0
  287. package/test/errors.test.ts +155 -0
  288. package/test/jwt-security.test.ts +182 -0
  289. package/test/jwt.test.ts +324 -0
  290. package/test/middleware.test.ts +300 -0
  291. package/test/password.test.ts +165 -0
  292. package/test/query-parser.test.ts +263 -0
  293. package/test/rate-limiter.test.ts +102 -0
  294. package/test/safe-compare.test.ts +66 -0
  295. package/test/singleton.test.ts +59 -0
  296. package/test/storage-local.test.ts +271 -0
  297. package/test/storage-registry.test.ts +282 -0
  298. package/test/storage-routes.test.ts +222 -0
  299. package/test/storage-s3.test.ts +304 -0
  300. package/test-ast.ts +28 -0
  301. package/test.ts +6 -0
  302. package/test_output.txt +1133 -0
  303. package/tsconfig.json +49 -0
  304. package/tsconfig.prod.json +20 -0
  305. package/vite.config.ts +80 -0
@@ -0,0 +1,547 @@
1
+ import { describe, it, expect, beforeEach, afterEach, jest } from "@jest/globals";
2
+ import { CronScheduler, validateCronExpression } from "./cron-scheduler";
3
+ import type { CronJobDefinition } from "@rebasepro/types";
4
+ import type { LoadedCronJob } from "./cron-loader";
5
+
6
+ // ─── Helpers ────────────────────────────────────────────────────────
7
+
8
+ function makeJob(id: string, overrides: Partial<CronJobDefinition> = {}): LoadedCronJob {
9
+ return {
10
+ id,
11
+ definition: {
12
+ schedule: "0 * * * *",
13
+ name: `Job ${id}`,
14
+ description: `Description for ${id}`,
15
+ enabled: true,
16
+ timeoutSeconds: 5,
17
+ handler: async (ctx) => { ctx.log("hello from", id); return { ok: true }; },
18
+ ...overrides
19
+ }
20
+ };
21
+ }
22
+
23
+ function makeFailingJob(id: string, errorMsg = "boom"): LoadedCronJob {
24
+ return makeJob(id, { handler: async () => { throw new Error(errorMsg); } });
25
+ }
26
+
27
+ // ─── Tests ──────────────────────────────────────────────────────────
28
+
29
+ describe("CronScheduler", () => {
30
+ let scheduler: CronScheduler;
31
+
32
+ beforeEach(() => {
33
+ scheduler = new CronScheduler();
34
+ jest.useFakeTimers();
35
+ });
36
+
37
+ afterEach(() => {
38
+ scheduler.stop();
39
+ jest.useRealTimers();
40
+ });
41
+
42
+ // ── validateCronExpression ───────────────────────────────────────
43
+
44
+ describe("validateCronExpression", () => {
45
+ it.each([
46
+ "0 * * * *", "*/5 * * * *", "0 0 1 * *", "30 2 * * 1",
47
+ "0 0 * * 0", "0,15,30,45 * * * *", "0 0 1-15 * *",
48
+ ])("accepts valid expression: %s", (expr) => {
49
+ expect(validateCronExpression(expr)).toEqual({ valid: true });
50
+ });
51
+
52
+ it("rejects empty string", () => {
53
+ const r = validateCronExpression("");
54
+ expect(r.valid).toBe(false);
55
+ });
56
+
57
+ it("rejects wrong field count", () => {
58
+ expect(validateCronExpression("* * *").valid).toBe(false);
59
+ expect(validateCronExpression("* * * * * *").valid).toBe(false);
60
+ });
61
+
62
+ it("rejects out-of-range values", () => {
63
+ expect(validateCronExpression("60 * * * *").valid).toBe(false);
64
+ expect(validateCronExpression("* 25 * * *").valid).toBe(false);
65
+ expect(validateCronExpression("* * 32 * *").valid).toBe(false);
66
+ expect(validateCronExpression("* * * 13 *").valid).toBe(false);
67
+ expect(validateCronExpression("* * * * 7").valid).toBe(false);
68
+ });
69
+
70
+ it("rejects non-numeric garbage", () => {
71
+ expect(validateCronExpression("abc * * * *").valid).toBe(false);
72
+ });
73
+ });
74
+
75
+ // ── Registration ────────────────────────────────────────────────
76
+
77
+ describe("registerJobs", () => {
78
+ it("registers jobs and they appear in listJobs", () => {
79
+ scheduler.registerJobs([makeJob("alpha"), makeJob("beta")]);
80
+ expect(scheduler.listJobs()).toHaveLength(2);
81
+ expect(scheduler.listJobs().map((j) => j.id).sort()).toEqual(["alpha", "beta"]);
82
+ });
83
+
84
+ it("sets initial state to idle for enabled jobs", () => {
85
+ scheduler.registerJobs([makeJob("enabled-job")]);
86
+ expect(scheduler.getJob("enabled-job")?.state).toBe("idle");
87
+ });
88
+
89
+ it("sets initial state to disabled for disabled jobs", () => {
90
+ scheduler.registerJobs([makeJob("disabled-job", { enabled: false })]);
91
+ expect(scheduler.getJob("disabled-job")?.state).toBe("disabled");
92
+ });
93
+
94
+ it("initializes counters to zero", () => {
95
+ scheduler.registerJobs([makeJob("fresh")]);
96
+ const job = scheduler.getJob("fresh")!;
97
+ expect(job.totalRuns).toBe(0);
98
+ expect(job.totalFailures).toBe(0);
99
+ });
100
+
101
+ it("overwrites duplicate job IDs", () => {
102
+ scheduler.registerJobs([makeJob("dup", { name: "First" })]);
103
+ scheduler.registerJobs([makeJob("dup", { name: "Second" })]);
104
+ expect(scheduler.listJobs()).toHaveLength(1);
105
+ expect(scheduler.listJobs()[0].name).toBe("Second");
106
+ });
107
+
108
+ it("preserves definition metadata", () => {
109
+ scheduler.registerJobs([makeJob("meta", { name: "My Job", description: "Desc", schedule: "30 2 * * 1" })]);
110
+ const job = scheduler.getJob("meta")!;
111
+ expect(job.name).toBe("My Job");
112
+ expect(job.description).toBe("Desc");
113
+ expect(job.schedule).toBe("30 2 * * 1");
114
+ });
115
+
116
+ it("rejects jobs with invalid cron schedules", () => {
117
+ scheduler.registerJobs([makeJob("bad", { schedule: "99 99 * * *" })]);
118
+ expect(scheduler.getJob("bad")).toBeUndefined();
119
+ expect(scheduler.listJobs()).toHaveLength(0);
120
+ });
121
+
122
+ it("rejects jobs with too few fields", () => {
123
+ scheduler.registerJobs([makeJob("short", { schedule: "* *" })]);
124
+ expect(scheduler.getJob("short")).toBeUndefined();
125
+ });
126
+
127
+ it("auto-schedules newly registered jobs if already started", () => {
128
+ scheduler.registerJobs([makeJob("early")]);
129
+ scheduler.start();
130
+ // Register after start
131
+ scheduler.registerJobs([makeJob("late")]);
132
+ expect(scheduler.getJob("late")?.nextRunAt).toBeDefined();
133
+ });
134
+
135
+ it("does NOT auto-schedule disabled jobs registered after start", () => {
136
+ scheduler.start();
137
+ scheduler.registerJobs([makeJob("off", { enabled: false })]);
138
+ expect(scheduler.getJob("off")?.nextRunAt).toBeUndefined();
139
+ });
140
+ });
141
+
142
+ // ── getJob ───────────────────────────────────────────────────────
143
+
144
+ describe("getJob", () => {
145
+ it("returns undefined for nonexistent ID", () => {
146
+ expect(scheduler.getJob("nope")).toBeUndefined();
147
+ });
148
+
149
+ it("returns the correct job by ID", () => {
150
+ scheduler.registerJobs([makeJob("a"), makeJob("b")]);
151
+ expect(scheduler.getJob("b")?.id).toBe("b");
152
+ });
153
+ });
154
+
155
+ // ── triggerJob (manual execution) ───────────────────────────────
156
+
157
+ describe("triggerJob", () => {
158
+ beforeEach(() => { jest.useRealTimers(); });
159
+
160
+ it("returns undefined for nonexistent job", async () => {
161
+ expect(await scheduler.triggerJob("ghost")).toBeUndefined();
162
+ });
163
+
164
+ it("executes the handler and returns a log entry", async () => {
165
+ scheduler.registerJobs([makeJob("trigger-me")]);
166
+ const log = await scheduler.triggerJob("trigger-me");
167
+ expect(log).toBeDefined();
168
+ expect(log!.jobId).toBe("trigger-me");
169
+ expect(log!.success).toBe(true);
170
+ expect(log!.manual).toBe(true);
171
+ expect(log!.durationMs).toBeGreaterThanOrEqual(0);
172
+ expect(log!.logs).toContain("hello from trigger-me");
173
+ expect(log!.result).toEqual({ ok: true });
174
+ });
175
+
176
+ it("increments totalRuns after trigger", async () => {
177
+ scheduler.registerJobs([makeJob("count-me")]);
178
+ await scheduler.triggerJob("count-me");
179
+ expect(scheduler.getJob("count-me")?.totalRuns).toBe(1);
180
+ await scheduler.triggerJob("count-me");
181
+ expect(scheduler.getJob("count-me")?.totalRuns).toBe(2);
182
+ });
183
+
184
+ it("records failure and increments totalFailures", async () => {
185
+ scheduler.registerJobs([makeFailingJob("fail-me", "something broke")]);
186
+ const log = await scheduler.triggerJob("fail-me");
187
+ expect(log!.success).toBe(false);
188
+ expect(log!.error).toBe("something broke");
189
+ const status = scheduler.getJob("fail-me")!;
190
+ expect(status.totalFailures).toBe(1);
191
+ expect(status.state).toBe("error");
192
+ expect(status.lastError).toBe("something broke");
193
+ });
194
+
195
+ it("captures ctx.log output", async () => {
196
+ scheduler.registerJobs([makeJob("logger", {
197
+ handler: async (ctx) => { ctx.log("line 1"); ctx.log("line 2", { nested: true }); ctx.log(42); }
198
+ })]);
199
+ const log = await scheduler.triggerJob("logger");
200
+ expect(log!.logs).toEqual(["line 1", 'line 2 {"nested":true}', "42"]);
201
+ });
202
+
203
+ it("sets lastRunAt after execution", async () => {
204
+ scheduler.registerJobs([makeJob("timed")]);
205
+ const before = new Date();
206
+ await scheduler.triggerJob("timed");
207
+ const after = new Date();
208
+ const job = scheduler.getJob("timed")!;
209
+ const lastRun = new Date(job.lastRunAt!);
210
+ expect(lastRun.getTime()).toBeGreaterThanOrEqual(before.getTime());
211
+ expect(lastRun.getTime()).toBeLessThanOrEqual(after.getTime());
212
+ });
213
+
214
+ it("sets lastDurationMs after execution", async () => {
215
+ scheduler.registerJobs([makeJob("duration-check")]);
216
+ await scheduler.triggerJob("duration-check");
217
+ expect(scheduler.getJob("duration-check")!.lastDurationMs!).toBeGreaterThanOrEqual(0);
218
+ });
219
+
220
+ it("handles handler that returns undefined (void)", async () => {
221
+ scheduler.registerJobs([makeJob("void-handler", { handler: async () => {} })]);
222
+ const log = await scheduler.triggerJob("void-handler");
223
+ expect(log!.success).toBe(true);
224
+ expect(log!.result).toBeUndefined();
225
+ });
226
+
227
+ it("handles synchronous handler", async () => {
228
+ scheduler.registerJobs([makeJob("sync-handler", {
229
+ handler: (ctx) => { ctx.log("sync"); return { sync: true }; }
230
+ })]);
231
+ const log = await scheduler.triggerJob("sync-handler");
232
+ expect(log!.success).toBe(true);
233
+ expect(log!.result).toEqual({ sync: true });
234
+ });
235
+
236
+ it("handles non-Error thrown values", async () => {
237
+ scheduler.registerJobs([makeJob("string-throw", {
238
+ handler: async () => { throw "plain string error"; }
239
+ })]);
240
+ const log = await scheduler.triggerJob("string-throw");
241
+ expect(log!.success).toBe(false);
242
+ expect(log!.error).toBe("plain string error");
243
+ });
244
+ });
245
+
246
+ // ── Concurrency guard ───────────────────────────────────────────
247
+
248
+ describe("concurrency guard", () => {
249
+ beforeEach(() => { jest.useRealTimers(); });
250
+
251
+ it("prevents overlapping manual triggers", async () => {
252
+ let resolve: () => void;
253
+ const blocker = new Promise<void>((r) => { resolve = r; });
254
+
255
+ scheduler.registerJobs([makeJob("slow", {
256
+ handler: async () => { await blocker; return { done: true }; }
257
+ })]);
258
+
259
+ // Start first trigger (will block)
260
+ const first = scheduler.triggerJob("slow");
261
+
262
+ // Try second trigger while first is running
263
+ const second = await scheduler.triggerJob("slow");
264
+
265
+ expect(second!.result).toEqual({ skipped: true, reason: "already_executing" });
266
+ expect(second!.logs).toContain("Skipped: job is already running");
267
+
268
+ // Let the first one finish
269
+ resolve!();
270
+ const firstResult = await first;
271
+ expect(firstResult!.success).toBe(true);
272
+ expect(firstResult!.result).toEqual({ done: true });
273
+ });
274
+
275
+ it("allows trigger after previous completes", async () => {
276
+ scheduler.registerJobs([makeJob("sequential")]);
277
+ const log1 = await scheduler.triggerJob("sequential");
278
+ const log2 = await scheduler.triggerJob("sequential");
279
+ expect(log1!.success).toBe(true);
280
+ expect(log2!.success).toBe(true);
281
+ expect(scheduler.getJob("sequential")!.totalRuns).toBe(2);
282
+ });
283
+
284
+ it("resets executing flag even after handler throws", async () => {
285
+ scheduler.registerJobs([makeFailingJob("crasher")]);
286
+ await scheduler.triggerJob("crasher");
287
+ // Should NOT be skipped — executing flag was reset
288
+ const log = await scheduler.triggerJob("crasher");
289
+ expect(log!.error).toBe("boom"); // ran again, not skipped
290
+ });
291
+ });
292
+
293
+ // ── Timeout ─────────────────────────────────────────────────────
294
+
295
+ describe("timeout", () => {
296
+ it("times out a slow handler", async () => {
297
+ jest.useRealTimers();
298
+ scheduler.registerJobs([makeJob("slow", {
299
+ timeoutSeconds: 1,
300
+ handler: () => new Promise((resolve) => setTimeout(() => resolve("late"), 3000))
301
+ })]);
302
+ const log = await scheduler.triggerJob("slow");
303
+ expect(log!.success).toBe(false);
304
+ expect(log!.error).toContain("timed out");
305
+ }, 10000);
306
+
307
+ it("clears timeout timer after success (no timer leak)", async () => {
308
+ jest.useRealTimers();
309
+ scheduler.registerJobs([makeJob("fast", {
310
+ timeoutSeconds: 60,
311
+ handler: async () => "quick"
312
+ })]);
313
+ const log = await scheduler.triggerJob("fast");
314
+ expect(log!.success).toBe(true);
315
+ // If timeout wasn't cleared, Jest would hang
316
+ });
317
+ });
318
+
319
+ // ── Logs ring buffer ────────────────────────────────────────────
320
+
321
+ describe("getJobLogs", () => {
322
+ beforeEach(() => { jest.useRealTimers(); });
323
+
324
+ it("returns empty array for nonexistent job", () => {
325
+ expect(scheduler.getJobLogs("nope")).toEqual([]);
326
+ });
327
+
328
+ it("returns logs in reverse order (newest first)", async () => {
329
+ scheduler.registerJobs([makeJob("ordered")]);
330
+ await scheduler.triggerJob("ordered");
331
+ await scheduler.triggerJob("ordered");
332
+ await scheduler.triggerJob("ordered");
333
+ const logs = scheduler.getJobLogs("ordered");
334
+ expect(logs).toHaveLength(3);
335
+ expect(new Date(logs[0].startedAt).getTime()).toBeGreaterThanOrEqual(
336
+ new Date(logs[2].startedAt).getTime()
337
+ );
338
+ });
339
+
340
+ it("respects limit parameter", async () => {
341
+ scheduler.registerJobs([makeJob("limited")]);
342
+ for (let i = 0; i < 5; i++) await scheduler.triggerJob("limited");
343
+ expect(scheduler.getJobLogs("limited", 2)).toHaveLength(2);
344
+ expect(scheduler.getJobLogs("limited", 10)).toHaveLength(5);
345
+ expect(scheduler.getJobLogs("limited")).toHaveLength(5);
346
+ });
347
+
348
+ it("caps at 50 entries (ring buffer)", async () => {
349
+ scheduler.registerJobs([makeJob("ring")]);
350
+ for (let i = 0; i < 60; i++) await scheduler.triggerJob("ring");
351
+ expect(scheduler.getJobLogs("ring")).toHaveLength(50);
352
+ expect(scheduler.getJob("ring")?.totalRuns).toBe(60);
353
+ });
354
+ });
355
+
356
+ // ── Enable / Disable ────────────────────────────────────────────
357
+
358
+ describe("setJobEnabled", () => {
359
+ it("returns undefined for nonexistent job", () => {
360
+ expect(scheduler.setJobEnabled("nope", true)).toBeUndefined();
361
+ });
362
+
363
+ it("disables a job", () => {
364
+ scheduler.registerJobs([makeJob("togglable")]);
365
+ const result = scheduler.setJobEnabled("togglable", false);
366
+ expect(result?.enabled).toBe(false);
367
+ expect(result?.state).toBe("disabled");
368
+ });
369
+
370
+ it("re-enables a disabled job", () => {
371
+ scheduler.registerJobs([makeJob("togglable")]);
372
+ scheduler.setJobEnabled("togglable", false);
373
+ scheduler.start();
374
+ const result = scheduler.setJobEnabled("togglable", true);
375
+ expect(result?.enabled).toBe(true);
376
+ expect(result?.state).toBe("idle");
377
+ });
378
+
379
+ it("clears nextRunAt when disabling", () => {
380
+ scheduler.registerJobs([makeJob("dis")]);
381
+ scheduler.start();
382
+ expect(scheduler.getJob("dis")?.nextRunAt).toBeDefined();
383
+ scheduler.setJobEnabled("dis", false);
384
+ expect(scheduler.getJob("dis")?.nextRunAt).toBeUndefined();
385
+ });
386
+
387
+ it("sets nextRunAt when re-enabling", () => {
388
+ scheduler.registerJobs([makeJob("reenable")]);
389
+ scheduler.start();
390
+ scheduler.setJobEnabled("reenable", false);
391
+ scheduler.setJobEnabled("reenable", true);
392
+ expect(scheduler.getJob("reenable")?.nextRunAt).toBeDefined();
393
+ });
394
+ });
395
+
396
+ // ── start / stop ────────────────────────────────────────────────
397
+
398
+ describe("start / stop", () => {
399
+ it("start is idempotent", () => {
400
+ scheduler.registerJobs([makeJob("idem")]);
401
+ scheduler.start();
402
+ scheduler.start();
403
+ expect(scheduler.listJobs()).toHaveLength(1);
404
+ });
405
+
406
+ it("stop clears nextRunAt", () => {
407
+ scheduler.registerJobs([makeJob("stoppable")]);
408
+ scheduler.start();
409
+ expect(scheduler.getJob("stoppable")?.nextRunAt).toBeDefined();
410
+ scheduler.stop();
411
+ expect(scheduler.getJob("stoppable")?.nextRunAt).toBeUndefined();
412
+ });
413
+
414
+ it("does not schedule disabled jobs on start", () => {
415
+ scheduler.registerJobs([makeJob("off", { enabled: false })]);
416
+ scheduler.start();
417
+ expect(scheduler.getJob("off")?.nextRunAt).toBeUndefined();
418
+ });
419
+
420
+ it("stop then start re-schedules jobs", () => {
421
+ scheduler.registerJobs([makeJob("restart")]);
422
+ scheduler.start();
423
+ scheduler.stop();
424
+ expect(scheduler.getJob("restart")?.nextRunAt).toBeUndefined();
425
+ // Reset started flag by creating a new scheduler with same jobs
426
+ const s2 = new CronScheduler();
427
+ s2.registerJobs([makeJob("restart")]);
428
+ s2.start();
429
+ expect(s2.getJob("restart")?.nextRunAt).toBeDefined();
430
+ s2.stop();
431
+ });
432
+ });
433
+
434
+ // ── Schedule-driven execution ───────────────────────────────────
435
+
436
+ describe("scheduled execution", () => {
437
+ it("fires job when timer elapses", async () => {
438
+ let executed = false;
439
+ scheduler.registerJobs([makeJob("timer-test", {
440
+ handler: async () => { executed = true; }
441
+ })]);
442
+ scheduler.start();
443
+ // Advance past next scheduled time
444
+ await jest.advanceTimersByTimeAsync(61 * 60 * 1000);
445
+ expect(executed).toBe(true);
446
+ expect(scheduler.getJob("timer-test")?.totalRuns).toBeGreaterThanOrEqual(1);
447
+ });
448
+
449
+ it("does not fire disabled job when timer elapses", async () => {
450
+ let executed = false;
451
+ scheduler.registerJobs([makeJob("disabled-timer", {
452
+ handler: async () => { executed = true; }
453
+ })]);
454
+ scheduler.start();
455
+ scheduler.setJobEnabled("disabled-timer", false);
456
+ await jest.advanceTimersByTimeAsync(61 * 60 * 1000);
457
+ expect(executed).toBe(false);
458
+ });
459
+
460
+ it("does not fire after stop()", async () => {
461
+ let executed = false;
462
+ scheduler.registerJobs([makeJob("stopped-timer", {
463
+ handler: async () => { executed = true; }
464
+ })]);
465
+ scheduler.start();
466
+ scheduler.stop();
467
+ await jest.advanceTimersByTimeAsync(61 * 60 * 1000);
468
+ expect(executed).toBe(false);
469
+ });
470
+ });
471
+
472
+ // ── CronStore integration ───────────────────────────────────────
473
+
474
+ describe("store integration", () => {
475
+ beforeEach(() => { jest.useRealTimers(); });
476
+
477
+ it("persists logs to store after execution", async () => {
478
+ const insertLog = jest.fn<(entry: any) => Promise<void>>().mockResolvedValue(undefined);
479
+ const mockStore = {
480
+ ensureTable: jest.fn<() => Promise<void>>().mockResolvedValue(undefined),
481
+ insertLog,
482
+ fetchLogs: jest.fn<() => Promise<any[]>>().mockResolvedValue([]),
483
+ fetchJobStats: jest.fn<() => Promise<Map<string, any>>>().mockResolvedValue(new Map()),
484
+ };
485
+ scheduler.setStore(mockStore);
486
+ scheduler.registerJobs([makeJob("persisted")]);
487
+ await scheduler.triggerJob("persisted");
488
+ expect(insertLog).toHaveBeenCalledTimes(1);
489
+ expect(insertLog.mock.calls[0]![0].jobId).toBe("persisted");
490
+ });
491
+
492
+ it("does not crash if store.insertLog fails", async () => {
493
+ const mockStore = {
494
+ ensureTable: jest.fn<() => Promise<void>>().mockResolvedValue(undefined),
495
+ insertLog: jest.fn<() => Promise<void>>().mockRejectedValue(new Error("DB down")),
496
+ fetchLogs: jest.fn<() => Promise<any[]>>().mockResolvedValue([]),
497
+ fetchJobStats: jest.fn<() => Promise<Map<string, any>>>().mockResolvedValue(new Map()),
498
+ };
499
+ scheduler.setStore(mockStore);
500
+ scheduler.registerJobs([makeJob("resilient")]);
501
+ // Should not throw
502
+ const log = await scheduler.triggerJob("resilient");
503
+ expect(log!.success).toBe(true);
504
+ });
505
+
506
+ it("seeds counters from store on start", async () => {
507
+ const stats = new Map<string, any>();
508
+ stats.set("seeded", { totalRuns: 42, totalFailures: 3, lastRunAt: "2026-01-01T00:00:00Z" });
509
+ const mockStore = {
510
+ ensureTable: jest.fn<() => Promise<void>>().mockResolvedValue(undefined),
511
+ insertLog: jest.fn<() => Promise<void>>().mockResolvedValue(undefined),
512
+ fetchLogs: jest.fn<() => Promise<any[]>>().mockResolvedValue([]),
513
+ fetchJobStats: jest.fn<() => Promise<Map<string, any>>>().mockResolvedValue(stats),
514
+ };
515
+ scheduler.setStore(mockStore);
516
+ scheduler.registerJobs([makeJob("seeded")]);
517
+ scheduler.start();
518
+ // Wait for async seed
519
+ await new Promise((r) => setTimeout(r, 50));
520
+ const job = scheduler.getJob("seeded")!;
521
+ expect(job.totalRuns).toBe(42);
522
+ expect(job.totalFailures).toBe(3);
523
+ });
524
+ });
525
+
526
+ // ── toStatus shape ──────────────────────────────────────────────
527
+
528
+ describe("status shape", () => {
529
+ it("returns all expected fields", () => {
530
+ scheduler.registerJobs([makeJob("shape", { name: "Shape Test", description: "Desc", schedule: "15 3 * * *" })]);
531
+ expect(scheduler.getJob("shape")).toMatchObject({
532
+ id: "shape", name: "Shape Test", description: "Desc",
533
+ schedule: "15 3 * * *", enabled: true, state: "idle",
534
+ totalRuns: 0, totalFailures: 0
535
+ });
536
+ });
537
+
538
+ it("lastRunAt and nextRunAt are ISO strings or undefined", () => {
539
+ scheduler.registerJobs([makeJob("iso-check")]);
540
+ expect(scheduler.getJob("iso-check")!.lastRunAt).toBeUndefined();
541
+ scheduler.start();
542
+ const after = scheduler.getJob("iso-check")!;
543
+ expect(after.nextRunAt).toBeDefined();
544
+ expect(() => new Date(after.nextRunAt!)).not.toThrow();
545
+ });
546
+ });
547
+ });