@oneuptime/common 12.0.0 → 12.0.2

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 (237) hide show
  1. package/Models/DatabaseModels/Host.ts +12 -4
  2. package/Models/DatabaseModels/KubernetesContainer.ts +5 -4
  3. package/Models/DatabaseModels/KubernetesResource.ts +8 -7
  4. package/Models/DatabaseModels/OnCallDutyPolicyScheduleLayer.ts +12 -2
  5. package/Models/DatabaseModels/PodmanResource.ts +6 -6
  6. package/Models/DatabaseModels/ServiceLevelObjective.ts +91 -0
  7. package/Models/DatabaseModels/StatusPage.ts +95 -0
  8. package/Server/API/TeamMemberAPI.ts +85 -0
  9. package/Server/Infrastructure/Postgres/LocalMigrationGenerationDataSource.ts +12 -2
  10. package/Server/Infrastructure/Postgres/SchemaMigrations/1785900000000-AddSloMonitorLabelRule.ts +84 -0
  11. package/Server/Infrastructure/Postgres/SchemaMigrations/1785915638551-IncreaseHostIpAddressesLength.ts +40 -0
  12. package/Server/Infrastructure/Postgres/SchemaMigrations/1785930000000-WidenInventoryResourceNameColumns.ts +131 -0
  13. package/Server/Infrastructure/Postgres/SchemaMigrations/1785930709405-AddStatusPageReportPeriod.ts +25 -0
  14. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +8 -0
  15. package/Server/Middleware/ProjectAuthorization.ts +66 -4
  16. package/Server/Services/BillingService.ts +600 -51
  17. package/Server/Services/CephClusterService.ts +47 -5
  18. package/Server/Services/CephResourceService.ts +43 -4
  19. package/Server/Services/CloudResourceService.ts +47 -5
  20. package/Server/Services/DatabaseService.ts +87 -6
  21. package/Server/Services/DockerHostService.ts +47 -5
  22. package/Server/Services/DockerResourceService.ts +62 -8
  23. package/Server/Services/DockerSwarmClusterService.ts +48 -5
  24. package/Server/Services/DockerSwarmResourceService.ts +53 -4
  25. package/Server/Services/HostService.ts +48 -5
  26. package/Server/Services/IoTDeviceService.ts +1 -17
  27. package/Server/Services/IoTFleetService.ts +47 -5
  28. package/Server/Services/KubernetesClusterService.ts +48 -5
  29. package/Server/Services/KubernetesContainerService.ts +50 -9
  30. package/Server/Services/KubernetesResourceService.ts +53 -7
  31. package/Server/Services/LabelService.ts +106 -1
  32. package/Server/Services/LlmProviderService.ts +69 -9
  33. package/Server/Services/MonitorService.ts +57 -0
  34. package/Server/Services/PodmanHostService.ts +47 -5
  35. package/Server/Services/PodmanResourceService.ts +62 -8
  36. package/Server/Services/ProjectService.ts +165 -35
  37. package/Server/Services/ProxmoxClusterService.ts +47 -5
  38. package/Server/Services/ProxmoxResourceService.ts +43 -4
  39. package/Server/Services/RumApplicationService.ts +48 -5
  40. package/Server/Services/RunnerService.ts +1 -7
  41. package/Server/Services/ServerlessFunctionService.ts +48 -5
  42. package/Server/Services/ServiceLevelObjectiveMonitorRuleEngineService.ts +426 -0
  43. package/Server/Services/ServiceLevelObjectiveService.ts +39 -0
  44. package/Server/Services/StatusPageService.ts +95 -36
  45. package/Server/Services/StatusPageSubscriberNotificationTemplateService.ts +18 -1
  46. package/Server/Services/StatusPageSubscriberService.ts +3 -0
  47. package/Server/Types/Database/JSONColumnQuery.ts +621 -0
  48. package/Server/Types/Database/Permissions/PublicPermission.ts +9 -2
  49. package/Server/Types/Database/QueryHelper.ts +17 -32
  50. package/Server/Utils/Database/TruncateColumnValue.ts +84 -0
  51. package/Server/Utils/Monitor/Criteria/CompareCriteria.ts +12 -0
  52. package/Tests/App/Dashboard/RunnerInstallInstructions.test.tsx +119 -0
  53. package/Tests/App/Dashboard/RunnerStatus.test.tsx +274 -0
  54. package/Tests/App/Dashboard/UsersTableGroupsByPerson.test.tsx +448 -0
  55. package/Tests/Server/API/BaseAPIApiKeyAuth.test.ts +704 -0
  56. package/Tests/Server/API/TeamMemberRemoveUserFromProjectAPI.test.ts +303 -0
  57. package/Tests/Server/Middleware/MasterAdminAuthorization.test.ts +350 -0
  58. package/Tests/Server/Middleware/ProjectAuthorizationApiKeyHeader.test.ts +362 -0
  59. package/Tests/Server/Middleware/ProjectAuthorizationApiKeyMiddleware.test.ts +687 -0
  60. package/Tests/Server/Services/BillingService.test.ts +78 -3
  61. package/Tests/Server/Services/BillingServiceChangePlanCancel.test.ts +645 -0
  62. package/Tests/Server/Services/BillingServiceTrialPlanChange.test.ts +1005 -0
  63. package/Tests/Server/Services/HookFreeWriteLengthClamp.test.ts +503 -0
  64. package/Tests/Server/Services/HostIpAddressesColumnWidth.test.ts +198 -0
  65. package/Tests/Server/Services/HostServiceUpdateLastSeen.test.ts +510 -0
  66. package/Tests/Server/Services/InventoryResourceNameColumnWidth.test.ts +500 -0
  67. package/Tests/Server/Services/InventoryUpsertSchemaParity.test.ts +586 -0
  68. package/Tests/Server/Services/InventoryUpsertTruncationGuard.test.ts +1242 -0
  69. package/Tests/Server/Services/LlmProviderUsableByProject.test.ts +293 -0
  70. package/Tests/Server/Services/ProjectServiceChangePlan.test.ts +650 -0
  71. package/Tests/Server/Services/ProjectServiceExtendTrial.test.ts +1 -0
  72. package/Tests/Server/Services/ResourceUpdateLastSeenLivenessFallback.test.ts +387 -0
  73. package/Tests/Server/Services/ServiceLevelObjectiveMonitorRuleEngineService.test.ts +770 -0
  74. package/Tests/Server/Services/ServiceLevelObjectiveService.test.ts +130 -0
  75. package/Tests/Server/Services/SloMonitorLabelRuleHooks.test.ts +352 -0
  76. package/Tests/Server/Services/StatusPageSubscriberReport.test.ts +29 -13
  77. package/Tests/Server/TestingUtils/Services/BillingServiceHelper.ts +69 -8
  78. package/Tests/Server/Types/Database/JSONColumnQuery.test.ts +593 -0
  79. package/Tests/Server/Types/Database/Permissions/PublicPermission.test.ts +495 -0
  80. package/Tests/Server/Types/Database/QueryUtil.test.ts +113 -0
  81. package/Tests/Server/Utils/AI/ToolArgsExtractors.test.ts +141 -0
  82. package/Tests/Server/Utils/AI/Toolbox/WidgetBuilder.test.ts +205 -0
  83. package/Tests/Server/Utils/Database/TruncateColumnValue.test.ts +179 -0
  84. package/Tests/Server/Utils/Monitor/Criteria/CompareCriteria.test.ts +897 -0
  85. package/Tests/Types/Events/Recurring.test.ts +157 -0
  86. package/Tests/Types/Runner/RunnerLiveStatus.test.ts +320 -0
  87. package/Tests/UI/Components/AiInvestigationSettingsCard.test.tsx +285 -0
  88. package/Tests/UI/Components/Detail/EntityFields.test.tsx +166 -0
  89. package/Tests/UI/Components/ModelTable/ModelTableGroupsProjectUsers.test.tsx +375 -0
  90. package/Tests/UI/Components/ModelTable/useCustomFieldColumns.test.tsx +196 -0
  91. package/Tests/UI/Utils/ProjectUsersModelAPI.test.ts +739 -0
  92. package/Tests/UI/Utils/TeamMembersByUser.test.ts +633 -0
  93. package/Tests/Utils/StatusPage/ReportPeriod.test.ts +218 -0
  94. package/Tests/Utils/Telemetry/HostIpAddresses.test.ts +291 -0
  95. package/Types/CustomField/CustomFieldDefinition.ts +24 -0
  96. package/Types/Date.ts +112 -0
  97. package/Types/Events/Recurring.ts +99 -1
  98. package/Types/Runbook/RunbookStep.ts +15 -0
  99. package/Types/Runner/RunnerLiveStatus.ts +114 -0
  100. package/Types/StatusPage/StatusPageReport.ts +11 -0
  101. package/Types/StatusPage/StatusPageReportPeriodType.ts +21 -0
  102. package/UI/Components/Detail/Detail.tsx +155 -0
  103. package/UI/Components/ModelTable/CustomFieldColumns.tsx +2 -6
  104. package/UI/Components/ModelTable/useCustomFieldColumns.ts +21 -2
  105. package/UI/Utils/ModelAPI/ProjectUsersModelAPI.ts +305 -0
  106. package/UI/Utils/TeamMembersByUser.ts +302 -0
  107. package/Utils/StatusPage/ReportPeriod.ts +236 -0
  108. package/Utils/Telemetry/HostIpAddresses.ts +98 -0
  109. package/build/dist/Models/DatabaseModels/Host.js +12 -4
  110. package/build/dist/Models/DatabaseModels/Host.js.map +1 -1
  111. package/build/dist/Models/DatabaseModels/KubernetesContainer.js +4 -4
  112. package/build/dist/Models/DatabaseModels/KubernetesContainer.js.map +1 -1
  113. package/build/dist/Models/DatabaseModels/KubernetesResource.js +7 -7
  114. package/build/dist/Models/DatabaseModels/KubernetesResource.js.map +1 -1
  115. package/build/dist/Models/DatabaseModels/OnCallDutyPolicyScheduleLayer.js +16 -4
  116. package/build/dist/Models/DatabaseModels/OnCallDutyPolicyScheduleLayer.js.map +1 -1
  117. package/build/dist/Models/DatabaseModels/PodmanResource.js +6 -6
  118. package/build/dist/Models/DatabaseModels/PodmanResource.js.map +1 -1
  119. package/build/dist/Models/DatabaseModels/ServiceLevelObjective.js +87 -0
  120. package/build/dist/Models/DatabaseModels/ServiceLevelObjective.js.map +1 -1
  121. package/build/dist/Models/DatabaseModels/StatusPage.js +98 -0
  122. package/build/dist/Models/DatabaseModels/StatusPage.js.map +1 -1
  123. package/build/dist/Server/API/TeamMemberAPI.js +57 -2
  124. package/build/dist/Server/API/TeamMemberAPI.js.map +1 -1
  125. package/build/dist/Server/Infrastructure/Postgres/LocalMigrationGenerationDataSource.js +11 -1
  126. package/build/dist/Server/Infrastructure/Postgres/LocalMigrationGenerationDataSource.js.map +1 -1
  127. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785900000000-AddSloMonitorLabelRule.js +45 -0
  128. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785900000000-AddSloMonitorLabelRule.js.map +1 -0
  129. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785915638551-IncreaseHostIpAddressesLength.js +31 -0
  130. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785915638551-IncreaseHostIpAddressesLength.js.map +1 -0
  131. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785930000000-WidenInventoryResourceNameColumns.js +90 -0
  132. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785930000000-WidenInventoryResourceNameColumns.js.map +1 -0
  133. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785930709405-AddStatusPageReportPeriod.js +14 -0
  134. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785930709405-AddStatusPageReportPeriod.js.map +1 -0
  135. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +8 -0
  136. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  137. package/build/dist/Server/Middleware/ProjectAuthorization.js +59 -4
  138. package/build/dist/Server/Middleware/ProjectAuthorization.js.map +1 -1
  139. package/build/dist/Server/Services/BillingService.js +466 -36
  140. package/build/dist/Server/Services/BillingService.js.map +1 -1
  141. package/build/dist/Server/Services/CephClusterService.js +42 -5
  142. package/build/dist/Server/Services/CephClusterService.js.map +1 -1
  143. package/build/dist/Server/Services/CephResourceService.js +28 -3
  144. package/build/dist/Server/Services/CephResourceService.js.map +1 -1
  145. package/build/dist/Server/Services/CloudResourceService.js +42 -5
  146. package/build/dist/Server/Services/CloudResourceService.js.map +1 -1
  147. package/build/dist/Server/Services/DatabaseService.js +69 -2
  148. package/build/dist/Server/Services/DatabaseService.js.map +1 -1
  149. package/build/dist/Server/Services/DockerHostService.js +42 -5
  150. package/build/dist/Server/Services/DockerHostService.js.map +1 -1
  151. package/build/dist/Server/Services/DockerResourceService.js +34 -4
  152. package/build/dist/Server/Services/DockerResourceService.js.map +1 -1
  153. package/build/dist/Server/Services/DockerSwarmClusterService.js +43 -5
  154. package/build/dist/Server/Services/DockerSwarmClusterService.js.map +1 -1
  155. package/build/dist/Server/Services/DockerSwarmResourceService.js +28 -3
  156. package/build/dist/Server/Services/DockerSwarmResourceService.js.map +1 -1
  157. package/build/dist/Server/Services/HostService.js +43 -5
  158. package/build/dist/Server/Services/HostService.js.map +1 -1
  159. package/build/dist/Server/Services/IoTDeviceService.js +1 -8
  160. package/build/dist/Server/Services/IoTDeviceService.js.map +1 -1
  161. package/build/dist/Server/Services/IoTFleetService.js +42 -5
  162. package/build/dist/Server/Services/IoTFleetService.js.map +1 -1
  163. package/build/dist/Server/Services/KubernetesClusterService.js +43 -5
  164. package/build/dist/Server/Services/KubernetesClusterService.js.map +1 -1
  165. package/build/dist/Server/Services/KubernetesContainerService.js +29 -3
  166. package/build/dist/Server/Services/KubernetesContainerService.js.map +1 -1
  167. package/build/dist/Server/Services/KubernetesResourceService.js +32 -4
  168. package/build/dist/Server/Services/KubernetesResourceService.js.map +1 -1
  169. package/build/dist/Server/Services/LabelService.js +92 -0
  170. package/build/dist/Server/Services/LabelService.js.map +1 -1
  171. package/build/dist/Server/Services/LlmProviderService.js +61 -8
  172. package/build/dist/Server/Services/LlmProviderService.js.map +1 -1
  173. package/build/dist/Server/Services/MonitorService.js +47 -0
  174. package/build/dist/Server/Services/MonitorService.js.map +1 -1
  175. package/build/dist/Server/Services/PodmanHostService.js +42 -5
  176. package/build/dist/Server/Services/PodmanHostService.js.map +1 -1
  177. package/build/dist/Server/Services/PodmanResourceService.js +34 -4
  178. package/build/dist/Server/Services/PodmanResourceService.js.map +1 -1
  179. package/build/dist/Server/Services/ProjectService.js +135 -21
  180. package/build/dist/Server/Services/ProjectService.js.map +1 -1
  181. package/build/dist/Server/Services/ProxmoxClusterService.js +42 -5
  182. package/build/dist/Server/Services/ProxmoxClusterService.js.map +1 -1
  183. package/build/dist/Server/Services/ProxmoxResourceService.js +28 -3
  184. package/build/dist/Server/Services/ProxmoxResourceService.js.map +1 -1
  185. package/build/dist/Server/Services/RumApplicationService.js +43 -5
  186. package/build/dist/Server/Services/RumApplicationService.js.map +1 -1
  187. package/build/dist/Server/Services/RunnerService.js +1 -6
  188. package/build/dist/Server/Services/RunnerService.js.map +1 -1
  189. package/build/dist/Server/Services/ServerlessFunctionService.js +43 -5
  190. package/build/dist/Server/Services/ServerlessFunctionService.js.map +1 -1
  191. package/build/dist/Server/Services/ServiceLevelObjectiveMonitorRuleEngineService.js +347 -0
  192. package/build/dist/Server/Services/ServiceLevelObjectiveMonitorRuleEngineService.js.map +1 -0
  193. package/build/dist/Server/Services/ServiceLevelObjectiveService.js +33 -1
  194. package/build/dist/Server/Services/ServiceLevelObjectiveService.js.map +1 -1
  195. package/build/dist/Server/Services/StatusPageService.js +69 -46
  196. package/build/dist/Server/Services/StatusPageService.js.map +1 -1
  197. package/build/dist/Server/Services/StatusPageSubscriberNotificationTemplateService.js +17 -1
  198. package/build/dist/Server/Services/StatusPageSubscriberNotificationTemplateService.js.map +1 -1
  199. package/build/dist/Server/Services/StatusPageSubscriberService.js +3 -0
  200. package/build/dist/Server/Services/StatusPageSubscriberService.js.map +1 -1
  201. package/build/dist/Server/Types/Database/JSONColumnQuery.js +402 -0
  202. package/build/dist/Server/Types/Database/JSONColumnQuery.js.map +1 -0
  203. package/build/dist/Server/Types/Database/Permissions/PublicPermission.js +9 -2
  204. package/build/dist/Server/Types/Database/Permissions/PublicPermission.js.map +1 -1
  205. package/build/dist/Server/Types/Database/QueryHelper.js +17 -27
  206. package/build/dist/Server/Types/Database/QueryHelper.js.map +1 -1
  207. package/build/dist/Server/Utils/Database/TruncateColumnValue.js +32 -0
  208. package/build/dist/Server/Utils/Database/TruncateColumnValue.js.map +1 -0
  209. package/build/dist/Server/Utils/Monitor/Criteria/CompareCriteria.js +11 -0
  210. package/build/dist/Server/Utils/Monitor/Criteria/CompareCriteria.js.map +1 -1
  211. package/build/dist/Types/CustomField/CustomFieldDefinition.js +2 -0
  212. package/build/dist/Types/CustomField/CustomFieldDefinition.js.map +1 -0
  213. package/build/dist/Types/Date.js +68 -0
  214. package/build/dist/Types/Date.js.map +1 -1
  215. package/build/dist/Types/Events/Recurring.js +63 -0
  216. package/build/dist/Types/Events/Recurring.js.map +1 -1
  217. package/build/dist/Types/Runbook/RunbookStep.js.map +1 -1
  218. package/build/dist/Types/Runner/RunnerLiveStatus.js +69 -0
  219. package/build/dist/Types/Runner/RunnerLiveStatus.js.map +1 -0
  220. package/build/dist/Types/StatusPage/StatusPageReportPeriodType.js +22 -0
  221. package/build/dist/Types/StatusPage/StatusPageReportPeriodType.js.map +1 -0
  222. package/build/dist/UI/Components/Detail/Detail.js +97 -0
  223. package/build/dist/UI/Components/Detail/Detail.js.map +1 -1
  224. package/build/dist/UI/Components/ModelTable/CustomFieldColumns.js.map +1 -1
  225. package/build/dist/UI/Components/ModelTable/useCustomFieldColumns.js +18 -2
  226. package/build/dist/UI/Components/ModelTable/useCustomFieldColumns.js.map +1 -1
  227. package/build/dist/UI/Utils/ModelAPI/ProjectUsersModelAPI.js +213 -0
  228. package/build/dist/UI/Utils/ModelAPI/ProjectUsersModelAPI.js.map +1 -0
  229. package/build/dist/UI/Utils/TeamMembersByUser.js +195 -0
  230. package/build/dist/UI/Utils/TeamMembersByUser.js.map +1 -0
  231. package/build/dist/Utils/StatusPage/ReportPeriod.js +131 -0
  232. package/build/dist/Utils/StatusPage/ReportPeriod.js.map +1 -0
  233. package/build/dist/Utils/Telemetry/HostIpAddresses.js +82 -0
  234. package/build/dist/Utils/Telemetry/HostIpAddresses.js.map +1 -0
  235. package/jest.config.json +2 -0
  236. package/package.json +1 -1
  237. package/tsconfig.json +12 -1
@@ -326,6 +326,163 @@ describe("Recurring", () => {
326
326
  });
327
327
  });
328
328
 
329
+ /*
330
+ * getNextDate approximates a month as 30.4375 days, which walks a monthly
331
+ * schedule backwards through the calendar (1 Jul -> 31 Jul -> 30 Aug) and
332
+ * eventually skips a month outright. getNextDateAfter steps in whole calendar
333
+ * units instead, which is what a status page report scheduled for "the 1st of
334
+ * every month" needs.
335
+ */
336
+ describe("getNextDateAfter", () => {
337
+ test("keeps a monthly schedule on the same day of the month", () => {
338
+ const start: Date = OneUptimeDate.fromString("2026-07-01T09:00:00.000Z");
339
+ const recurring: Recurring = makeRecurring(EventInterval.Month, 1);
340
+
341
+ let next: Date = Recurring.getNextDateAfter({
342
+ startDate: start,
343
+ recurring: recurring,
344
+ afterDate: OneUptimeDate.fromString("2026-07-01T09:00:01.000Z"),
345
+ timezone: "UTC",
346
+ });
347
+ expect(next.toISOString()).toBe("2026-08-01T09:00:00.000Z");
348
+
349
+ next = Recurring.getNextDateAfter({
350
+ startDate: next,
351
+ recurring: recurring,
352
+ afterDate: OneUptimeDate.fromString("2026-08-01T09:00:01.000Z"),
353
+ timezone: "UTC",
354
+ });
355
+ expect(next.toISOString()).toBe("2026-09-01T09:00:00.000Z");
356
+
357
+ next = Recurring.getNextDateAfter({
358
+ startDate: next,
359
+ recurring: recurring,
360
+ afterDate: OneUptimeDate.fromString("2026-09-01T09:00:01.000Z"),
361
+ timezone: "UTC",
362
+ });
363
+ expect(next.toISOString()).toBe("2026-10-01T09:00:00.000Z");
364
+ });
365
+
366
+ test("never skips a month, which the millisecond approximation does", () => {
367
+ const start: Date = OneUptimeDate.fromString("2026-01-01T09:00:00.000Z");
368
+
369
+ const next: Date = Recurring.getNextDateAfter({
370
+ startDate: start,
371
+ recurring: makeRecurring(EventInterval.Month, 1),
372
+ // A status page left alone for most of a year.
373
+ afterDate: OneUptimeDate.fromString("2026-11-15T00:00:00.000Z"),
374
+ timezone: "UTC",
375
+ });
376
+
377
+ expect(next.toISOString()).toBe("2026-12-01T09:00:00.000Z");
378
+ });
379
+
380
+ test("holds the local time of day across a DST transition", () => {
381
+ /*
382
+ * 1 Mar 2026 09:00 in New York is EST (UTC-5); 1 Apr is EDT (UTC-4).
383
+ * Adding a fixed number of milliseconds would land at 08:00 local.
384
+ */
385
+ const next: Date = Recurring.getNextDateAfter({
386
+ startDate: OneUptimeDate.fromString("2026-03-01T14:00:00.000Z"),
387
+ recurring: makeRecurring(EventInterval.Month, 1),
388
+ afterDate: OneUptimeDate.fromString("2026-03-15T00:00:00.000Z"),
389
+ timezone: "America/New_York",
390
+ });
391
+
392
+ expect(next.toISOString()).toBe("2026-04-01T13:00:00.000Z");
393
+ });
394
+
395
+ test("clamps a day-of-month that the next month does not have", () => {
396
+ const next: Date = Recurring.getNextDateAfter({
397
+ startDate: OneUptimeDate.fromString("2026-01-31T09:00:00.000Z"),
398
+ recurring: makeRecurring(EventInterval.Month, 1),
399
+ afterDate: OneUptimeDate.fromString("2026-02-01T00:00:00.000Z"),
400
+ timezone: "UTC",
401
+ });
402
+
403
+ expect(next.toISOString()).toBe("2026-02-28T09:00:00.000Z");
404
+ });
405
+
406
+ test("returns a start date that has not happened yet unchanged", () => {
407
+ const start: Date = OneUptimeDate.fromString("2026-08-01T09:00:00.000Z");
408
+
409
+ const next: Date = Recurring.getNextDateAfter({
410
+ startDate: start,
411
+ recurring: makeRecurring(EventInterval.Month, 1),
412
+ afterDate: OneUptimeDate.fromString("2026-07-15T00:00:00.000Z"),
413
+ timezone: "UTC",
414
+ });
415
+
416
+ expect(next.getTime()).toBe(start.getTime());
417
+ });
418
+
419
+ test("advances by whole intervals for a multi-unit recurrence", () => {
420
+ const next: Date = Recurring.getNextDateAfter({
421
+ startDate: OneUptimeDate.fromString("2026-01-01T00:00:00.000Z"),
422
+ recurring: makeRecurring(EventInterval.Month, 3),
423
+ afterDate: OneUptimeDate.fromString("2026-05-02T00:00:00.000Z"),
424
+ timezone: "UTC",
425
+ });
426
+
427
+ // Occurrences are 1 Jan, 1 Apr, 1 Jul - the first after 2 May is 1 Jul.
428
+ expect(next.toISOString()).toBe("2026-07-01T00:00:00.000Z");
429
+ });
430
+
431
+ test("handles hourly, daily, weekly and yearly recurrences", () => {
432
+ const start: Date = OneUptimeDate.fromString("2026-01-01T00:00:00.000Z");
433
+ const after: Date = OneUptimeDate.fromString("2026-01-01T05:30:00.000Z");
434
+
435
+ expect(
436
+ Recurring.getNextDateAfter({
437
+ startDate: start,
438
+ recurring: makeRecurring(EventInterval.Hour, 2),
439
+ afterDate: after,
440
+ timezone: "UTC",
441
+ }).toISOString(),
442
+ ).toBe("2026-01-01T06:00:00.000Z");
443
+
444
+ expect(
445
+ Recurring.getNextDateAfter({
446
+ startDate: start,
447
+ recurring: makeRecurring(EventInterval.Day, 1),
448
+ afterDate: after,
449
+ timezone: "UTC",
450
+ }).toISOString(),
451
+ ).toBe("2026-01-02T00:00:00.000Z");
452
+
453
+ expect(
454
+ Recurring.getNextDateAfter({
455
+ startDate: start,
456
+ recurring: makeRecurring(EventInterval.Week, 1),
457
+ afterDate: after,
458
+ timezone: "UTC",
459
+ }).toISOString(),
460
+ ).toBe("2026-01-08T00:00:00.000Z");
461
+
462
+ expect(
463
+ Recurring.getNextDateAfter({
464
+ startDate: start,
465
+ recurring: makeRecurring(EventInterval.Year, 1),
466
+ afterDate: after,
467
+ timezone: "UTC",
468
+ }).toISOString(),
469
+ ).toBe("2027-01-01T00:00:00.000Z");
470
+ });
471
+
472
+ test("throws BadDataException for an unknown interval type", () => {
473
+ const recurring: Recurring = makeRecurring(EventInterval.Day, 1);
474
+ recurring.intervalType = "Fortnight" as unknown as EventInterval;
475
+
476
+ expect(() => {
477
+ Recurring.getNextDateAfter({
478
+ startDate: baseDate,
479
+ recurring: recurring,
480
+ afterDate: OneUptimeDate.getCurrentDate(),
481
+ });
482
+ }).toThrowError(BadDataException);
483
+ });
484
+ });
485
+
329
486
  describe("toString", () => {
330
487
  test("formats as count and interval type", () => {
331
488
  expect(makeRecurring(EventInterval.Day, 1).toString()).toBe("1 Day");
@@ -0,0 +1,320 @@
1
+ import { describe, expect, test } from "@jest/globals";
2
+ import fs from "fs";
3
+ import path from "path";
4
+ import OneUptimeDate from "../../../Types/Date";
5
+ import RunnerLiveStatus, {
6
+ RUNNER_ALIVE_WINDOW_IN_MINUTES,
7
+ getRunnerLiveStatus,
8
+ getRunnerLiveStatusLabel,
9
+ } from "../../../Types/Runner/RunnerLiveStatus";
10
+
11
+ /*
12
+ * getRunnerLiveStatus is the whole reason the Runner UI stopped rendering the
13
+ * connectionStatus column. That column is written exactly twice in a Runner's
14
+ * life — Disconnected by RunnerService.onBeforeCreate, Connected by the first
15
+ * heartbeat — and nothing ever writes it again, so it reports every Runner
16
+ * that has ever connected as connected forever, and reports a Runner that was
17
+ * created five seconds ago and never started as disconnected. Both readings
18
+ * are wrong in the direction that costs an operator the most.
19
+ *
20
+ * These tests pin the replacement, and — the load-bearing half — pin that the
21
+ * dashboard and the server are still deciding "is this Runner alive" from the
22
+ * same constant.
23
+ */
24
+
25
+ type MinutesAgoFunction = (minutes: number) => Date;
26
+
27
+ const minutesAgo: MinutesAgoFunction = (minutes: number): Date => {
28
+ return OneUptimeDate.getSomeMinutesAgo(minutes);
29
+ };
30
+
31
+ describe("getRunnerLiveStatus", () => {
32
+ describe("never connected", () => {
33
+ test("undefined lastAlive is NeverConnected", () => {
34
+ expect(getRunnerLiveStatus(undefined)).toBe(
35
+ RunnerLiveStatus.NeverConnected,
36
+ );
37
+ });
38
+
39
+ test("null lastAlive is NeverConnected", () => {
40
+ expect(getRunnerLiveStatus(null)).toBe(RunnerLiveStatus.NeverConnected);
41
+ });
42
+
43
+ /*
44
+ * A Runner selected without the lastAlive column arrives with the field
45
+ * absent, which is the same shape as never having heartbeated. That is
46
+ * why both call sites pass lastAlive through selectMoreFields.
47
+ */
48
+ test("empty string lastAlive is NeverConnected", () => {
49
+ expect(getRunnerLiveStatus("")).toBe(RunnerLiveStatus.NeverConnected);
50
+ });
51
+ });
52
+
53
+ describe("connected", () => {
54
+ test("a heartbeat right now is Connected", () => {
55
+ expect(getRunnerLiveStatus(OneUptimeDate.getCurrentDate())).toBe(
56
+ RunnerLiveStatus.Connected,
57
+ );
58
+ });
59
+
60
+ test("a heartbeat one minute ago is Connected", () => {
61
+ expect(getRunnerLiveStatus(minutesAgo(1))).toBe(
62
+ RunnerLiveStatus.Connected,
63
+ );
64
+ });
65
+
66
+ /*
67
+ * The Runner heartbeats every 60s, so anything inside the window is a
68
+ * Runner that is polling for work right now.
69
+ */
70
+ test("a heartbeat just inside the window is Connected", () => {
71
+ expect(
72
+ getRunnerLiveStatus(minutesAgo(RUNNER_ALIVE_WINDOW_IN_MINUTES - 1)),
73
+ ).toBe(RunnerLiveStatus.Connected);
74
+ });
75
+
76
+ /*
77
+ * Clock skew between the Runner's host and the server can put lastAlive
78
+ * slightly in the future. That is a live Runner, not a dead one.
79
+ */
80
+ test("a lastAlive in the future is Connected", () => {
81
+ const tenMinutesFromNow: Date = new Date(Date.now() + 10 * 60 * 1000);
82
+
83
+ expect(getRunnerLiveStatus(tenMinutesFromNow)).toBe(
84
+ RunnerLiveStatus.Connected,
85
+ );
86
+ });
87
+ });
88
+
89
+ describe("disconnected", () => {
90
+ test("a heartbeat past the window is Disconnected", () => {
91
+ expect(
92
+ getRunnerLiveStatus(minutesAgo(RUNNER_ALIVE_WINDOW_IN_MINUTES + 1)),
93
+ ).toBe(RunnerLiveStatus.Disconnected);
94
+ });
95
+
96
+ test("a heartbeat an hour ago is Disconnected", () => {
97
+ expect(getRunnerLiveStatus(minutesAgo(60))).toBe(
98
+ RunnerLiveStatus.Disconnected,
99
+ );
100
+ });
101
+
102
+ test("a heartbeat a year ago is Disconnected", () => {
103
+ expect(getRunnerLiveStatus(minutesAgo(60 * 24 * 365))).toBe(
104
+ RunnerLiveStatus.Disconnected,
105
+ );
106
+ });
107
+
108
+ /*
109
+ * The boundary itself. isAfter compares at second granularity, so a
110
+ * timestamp exactly one window old is NOT after the window start and
111
+ * falls out of Connected. Pinned because the alternative — treating the
112
+ * boundary as alive — would disagree with the server, whose query is a
113
+ * strict greaterThan against the same instant.
114
+ */
115
+ test("a heartbeat exactly one window old is Disconnected", () => {
116
+ expect(
117
+ getRunnerLiveStatus(minutesAgo(RUNNER_ALIVE_WINDOW_IN_MINUTES)),
118
+ ).toBe(RunnerLiveStatus.Disconnected);
119
+ });
120
+ });
121
+
122
+ describe("input shapes", () => {
123
+ test("accepts a Date", () => {
124
+ expect(getRunnerLiveStatus(new Date())).toBe(RunnerLiveStatus.Connected);
125
+ });
126
+
127
+ /*
128
+ * The dashboard reads Runners out of a JSON API response, where dates
129
+ * are ISO strings and not Date objects.
130
+ */
131
+ test("accepts an ISO string", () => {
132
+ expect(getRunnerLiveStatus(new Date().toISOString())).toBe(
133
+ RunnerLiveStatus.Connected,
134
+ );
135
+ });
136
+
137
+ test("accepts a stale ISO string", () => {
138
+ expect(getRunnerLiveStatus(minutesAgo(120).toISOString())).toBe(
139
+ RunnerLiveStatus.Disconnected,
140
+ );
141
+ });
142
+
143
+ /*
144
+ * A value we cannot parse is not evidence of life. Reporting Connected
145
+ * here would paint a green dot on a Runner nobody has heard from.
146
+ */
147
+ test("an unparseable string is Disconnected, never Connected", () => {
148
+ expect(getRunnerLiveStatus("not-a-date")).toBe(
149
+ RunnerLiveStatus.Disconnected,
150
+ );
151
+ });
152
+
153
+ test("an Invalid Date is Disconnected, never Connected", () => {
154
+ expect(getRunnerLiveStatus(new Date("nonsense"))).toBe(
155
+ RunnerLiveStatus.Disconnected,
156
+ );
157
+ });
158
+ });
159
+
160
+ describe("the enum", () => {
161
+ test("has exactly three states", () => {
162
+ expect(Object.keys(RunnerLiveStatus).sort()).toEqual([
163
+ "Connected",
164
+ "Disconnected",
165
+ "NeverConnected",
166
+ ]);
167
+ });
168
+
169
+ /*
170
+ * NeverConnected must not collide with the persisted
171
+ * RunnerConnectionStatus wire values ("connected" / "disconnected"), so
172
+ * that nothing can round-trip one into the other by accident.
173
+ */
174
+ test("NeverConnected has its own distinct value", () => {
175
+ expect(RunnerLiveStatus.NeverConnected).toBe("never-connected");
176
+ expect(RunnerLiveStatus.NeverConnected).not.toBe(
177
+ RunnerLiveStatus.Disconnected,
178
+ );
179
+ expect(RunnerLiveStatus.NeverConnected).not.toBe(
180
+ RunnerLiveStatus.Connected,
181
+ );
182
+ });
183
+ });
184
+ });
185
+
186
+ /*
187
+ * The status-to-wording mapping. Both the status bubble and the runbook step
188
+ * editor's Runner picker label through this, so an inversion here would show
189
+ * a live Runner as never connected in two different places at once.
190
+ *
191
+ * Asserted branch by branch rather than by grepping for the three literals:
192
+ * a "the file contains all three words" check passes just as happily with the
193
+ * mapping inverted, which is exactly what an earlier version of this suite did.
194
+ */
195
+ describe("getRunnerLiveStatusLabel", () => {
196
+ test("Connected maps to 'Connected'", () => {
197
+ expect(getRunnerLiveStatusLabel(RunnerLiveStatus.Connected)).toBe(
198
+ "Connected",
199
+ );
200
+ });
201
+
202
+ test("Disconnected maps to 'Disconnected'", () => {
203
+ expect(getRunnerLiveStatusLabel(RunnerLiveStatus.Disconnected)).toBe(
204
+ "Disconnected",
205
+ );
206
+ });
207
+
208
+ test("NeverConnected maps to 'Never connected'", () => {
209
+ expect(getRunnerLiveStatusLabel(RunnerLiveStatus.NeverConnected)).toBe(
210
+ "Never connected",
211
+ );
212
+ });
213
+
214
+ test("the three labels are distinct", () => {
215
+ const labels: Array<string> = [
216
+ RunnerLiveStatus.Connected,
217
+ RunnerLiveStatus.Disconnected,
218
+ RunnerLiveStatus.NeverConnected,
219
+ ].map((status: RunnerLiveStatus): string => {
220
+ return getRunnerLiveStatusLabel(status);
221
+ });
222
+
223
+ expect(new Set(labels).size).toBe(3);
224
+ });
225
+
226
+ /*
227
+ * "Connected" and "Disconnected" are pre-existing i18next keys, translated
228
+ * in all sixteen locale files for the probe pages. Changing the casing or
229
+ * wording here would silently drop fifteen translations back to English.
230
+ */
231
+ test("it reuses the exact pre-existing locale keys", () => {
232
+ expect(getRunnerLiveStatusLabel(RunnerLiveStatus.Connected)).not.toBe(
233
+ "connected",
234
+ );
235
+ expect(getRunnerLiveStatusLabel(RunnerLiveStatus.Disconnected)).not.toBe(
236
+ "disconnected",
237
+ );
238
+ });
239
+
240
+ test("composes end to end from a lastAlive", () => {
241
+ expect(
242
+ getRunnerLiveStatusLabel(
243
+ getRunnerLiveStatus(OneUptimeDate.getCurrentDate()),
244
+ ),
245
+ ).toBe("Connected");
246
+
247
+ expect(getRunnerLiveStatusLabel(getRunnerLiveStatus(null))).toBe(
248
+ "Never connected",
249
+ );
250
+
251
+ expect(getRunnerLiveStatusLabel(getRunnerLiveStatus(minutesAgo(60)))).toBe(
252
+ "Disconnected",
253
+ );
254
+ });
255
+ });
256
+
257
+ describe("the alive window is shared with the server", () => {
258
+ const RUNNER_SERVICE: string = path.join(
259
+ __dirname,
260
+ "..",
261
+ "..",
262
+ "..",
263
+ "Server",
264
+ "Services",
265
+ "RunnerService.ts",
266
+ );
267
+
268
+ test("the window is five minutes", () => {
269
+ expect(RUNNER_ALIVE_WINDOW_IN_MINUTES).toBe(5);
270
+ });
271
+
272
+ /*
273
+ * The point of moving this constant into Types was that the dashboard and
274
+ * the server answer "is this Runner alive" identically. A server that hands
275
+ * work to a Runner the dashboard is drawing as Disconnected — or refuses
276
+ * work to one drawn as Connected — is worse than either answer alone, and
277
+ * that drift is invisible until someone is debugging a runbook that never
278
+ * ran. So: RunnerService must IMPORT the constant, not redeclare it.
279
+ */
280
+ test("RunnerService imports the constant instead of declaring its own", () => {
281
+ const source: string = fs.readFileSync(RUNNER_SERVICE, "utf8");
282
+
283
+ expect(source).toContain('from "../../Types/Runner/RunnerLiveStatus"');
284
+ expect(source).toContain("RUNNER_ALIVE_WINDOW_IN_MINUTES");
285
+
286
+ // No local redeclaration may shadow the shared one.
287
+ expect(source).not.toMatch(/const\s+RUNNER_ALIVE_WINDOW_IN_MINUTES\s*:/);
288
+ });
289
+
290
+ test("RunnerService still gates its queries on the shared window", () => {
291
+ const source: string = fs.readFileSync(RUNNER_SERVICE, "utf8");
292
+
293
+ const usages: number = (
294
+ source.match(
295
+ /OneUptimeDate\.getSomeMinutesAgo\(\s*RUNNER_ALIVE_WINDOW_IN_MINUTES,?\s*\)/g,
296
+ ) || []
297
+ ).length;
298
+
299
+ // getOnlineAiCommandRunnersForProject and getOnlineCodeFixRunnerForProject.
300
+ expect(usages).toBe(2);
301
+ });
302
+
303
+ /*
304
+ * Guards the reason the UI stopped trusting the column at all: if someone
305
+ * later adds a job that ages connectionStatus out, this test failing is the
306
+ * prompt to revisit the derived status.
307
+ */
308
+ test("nothing in RunnerService writes connectionStatus back to Disconnected", () => {
309
+ const source: string = fs.readFileSync(RUNNER_SERVICE, "utf8");
310
+
311
+ const disconnectedWrites: number = (
312
+ source.match(
313
+ /connectionStatus\s*[:=]\s*RunnerConnectionStatus\.Disconnected/g,
314
+ ) || []
315
+ ).length;
316
+
317
+ // Exactly one: the onBeforeCreate default. No ageing-out writer exists.
318
+ expect(disconnectedWrites).toBe(1);
319
+ });
320
+ });