@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.
- package/Models/DatabaseModels/Host.ts +12 -4
- package/Models/DatabaseModels/KubernetesContainer.ts +5 -4
- package/Models/DatabaseModels/KubernetesResource.ts +8 -7
- package/Models/DatabaseModels/OnCallDutyPolicyScheduleLayer.ts +12 -2
- package/Models/DatabaseModels/PodmanResource.ts +6 -6
- package/Models/DatabaseModels/ServiceLevelObjective.ts +91 -0
- package/Models/DatabaseModels/StatusPage.ts +95 -0
- package/Server/API/TeamMemberAPI.ts +85 -0
- package/Server/Infrastructure/Postgres/LocalMigrationGenerationDataSource.ts +12 -2
- package/Server/Infrastructure/Postgres/SchemaMigrations/1785900000000-AddSloMonitorLabelRule.ts +84 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1785915638551-IncreaseHostIpAddressesLength.ts +40 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1785930000000-WidenInventoryResourceNameColumns.ts +131 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1785930709405-AddStatusPageReportPeriod.ts +25 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +8 -0
- package/Server/Middleware/ProjectAuthorization.ts +66 -4
- package/Server/Services/BillingService.ts +600 -51
- package/Server/Services/CephClusterService.ts +47 -5
- package/Server/Services/CephResourceService.ts +43 -4
- package/Server/Services/CloudResourceService.ts +47 -5
- package/Server/Services/DatabaseService.ts +87 -6
- package/Server/Services/DockerHostService.ts +47 -5
- package/Server/Services/DockerResourceService.ts +62 -8
- package/Server/Services/DockerSwarmClusterService.ts +48 -5
- package/Server/Services/DockerSwarmResourceService.ts +53 -4
- package/Server/Services/HostService.ts +48 -5
- package/Server/Services/IoTDeviceService.ts +1 -17
- package/Server/Services/IoTFleetService.ts +47 -5
- package/Server/Services/KubernetesClusterService.ts +48 -5
- package/Server/Services/KubernetesContainerService.ts +50 -9
- package/Server/Services/KubernetesResourceService.ts +53 -7
- package/Server/Services/LabelService.ts +106 -1
- package/Server/Services/LlmProviderService.ts +69 -9
- package/Server/Services/MonitorService.ts +57 -0
- package/Server/Services/PodmanHostService.ts +47 -5
- package/Server/Services/PodmanResourceService.ts +62 -8
- package/Server/Services/ProjectService.ts +165 -35
- package/Server/Services/ProxmoxClusterService.ts +47 -5
- package/Server/Services/ProxmoxResourceService.ts +43 -4
- package/Server/Services/RumApplicationService.ts +48 -5
- package/Server/Services/RunnerService.ts +1 -7
- package/Server/Services/ServerlessFunctionService.ts +48 -5
- package/Server/Services/ServiceLevelObjectiveMonitorRuleEngineService.ts +426 -0
- package/Server/Services/ServiceLevelObjectiveService.ts +39 -0
- package/Server/Services/StatusPageService.ts +95 -36
- package/Server/Services/StatusPageSubscriberNotificationTemplateService.ts +18 -1
- package/Server/Services/StatusPageSubscriberService.ts +3 -0
- package/Server/Types/Database/JSONColumnQuery.ts +621 -0
- package/Server/Types/Database/Permissions/PublicPermission.ts +9 -2
- package/Server/Types/Database/QueryHelper.ts +17 -32
- package/Server/Utils/Database/TruncateColumnValue.ts +84 -0
- package/Server/Utils/Monitor/Criteria/CompareCriteria.ts +12 -0
- package/Tests/App/Dashboard/RunnerInstallInstructions.test.tsx +119 -0
- package/Tests/App/Dashboard/RunnerStatus.test.tsx +274 -0
- package/Tests/App/Dashboard/UsersTableGroupsByPerson.test.tsx +448 -0
- package/Tests/Server/API/BaseAPIApiKeyAuth.test.ts +704 -0
- package/Tests/Server/API/TeamMemberRemoveUserFromProjectAPI.test.ts +303 -0
- package/Tests/Server/Middleware/MasterAdminAuthorization.test.ts +350 -0
- package/Tests/Server/Middleware/ProjectAuthorizationApiKeyHeader.test.ts +362 -0
- package/Tests/Server/Middleware/ProjectAuthorizationApiKeyMiddleware.test.ts +687 -0
- package/Tests/Server/Services/BillingService.test.ts +78 -3
- package/Tests/Server/Services/BillingServiceChangePlanCancel.test.ts +645 -0
- package/Tests/Server/Services/BillingServiceTrialPlanChange.test.ts +1005 -0
- package/Tests/Server/Services/HookFreeWriteLengthClamp.test.ts +503 -0
- package/Tests/Server/Services/HostIpAddressesColumnWidth.test.ts +198 -0
- package/Tests/Server/Services/HostServiceUpdateLastSeen.test.ts +510 -0
- package/Tests/Server/Services/InventoryResourceNameColumnWidth.test.ts +500 -0
- package/Tests/Server/Services/InventoryUpsertSchemaParity.test.ts +586 -0
- package/Tests/Server/Services/InventoryUpsertTruncationGuard.test.ts +1242 -0
- package/Tests/Server/Services/LlmProviderUsableByProject.test.ts +293 -0
- package/Tests/Server/Services/ProjectServiceChangePlan.test.ts +650 -0
- package/Tests/Server/Services/ProjectServiceExtendTrial.test.ts +1 -0
- package/Tests/Server/Services/ResourceUpdateLastSeenLivenessFallback.test.ts +387 -0
- package/Tests/Server/Services/ServiceLevelObjectiveMonitorRuleEngineService.test.ts +770 -0
- package/Tests/Server/Services/ServiceLevelObjectiveService.test.ts +130 -0
- package/Tests/Server/Services/SloMonitorLabelRuleHooks.test.ts +352 -0
- package/Tests/Server/Services/StatusPageSubscriberReport.test.ts +29 -13
- package/Tests/Server/TestingUtils/Services/BillingServiceHelper.ts +69 -8
- package/Tests/Server/Types/Database/JSONColumnQuery.test.ts +593 -0
- package/Tests/Server/Types/Database/Permissions/PublicPermission.test.ts +495 -0
- package/Tests/Server/Types/Database/QueryUtil.test.ts +113 -0
- package/Tests/Server/Utils/AI/ToolArgsExtractors.test.ts +141 -0
- package/Tests/Server/Utils/AI/Toolbox/WidgetBuilder.test.ts +205 -0
- package/Tests/Server/Utils/Database/TruncateColumnValue.test.ts +179 -0
- package/Tests/Server/Utils/Monitor/Criteria/CompareCriteria.test.ts +897 -0
- package/Tests/Types/Events/Recurring.test.ts +157 -0
- package/Tests/Types/Runner/RunnerLiveStatus.test.ts +320 -0
- package/Tests/UI/Components/AiInvestigationSettingsCard.test.tsx +285 -0
- package/Tests/UI/Components/Detail/EntityFields.test.tsx +166 -0
- package/Tests/UI/Components/ModelTable/ModelTableGroupsProjectUsers.test.tsx +375 -0
- package/Tests/UI/Components/ModelTable/useCustomFieldColumns.test.tsx +196 -0
- package/Tests/UI/Utils/ProjectUsersModelAPI.test.ts +739 -0
- package/Tests/UI/Utils/TeamMembersByUser.test.ts +633 -0
- package/Tests/Utils/StatusPage/ReportPeriod.test.ts +218 -0
- package/Tests/Utils/Telemetry/HostIpAddresses.test.ts +291 -0
- package/Types/CustomField/CustomFieldDefinition.ts +24 -0
- package/Types/Date.ts +112 -0
- package/Types/Events/Recurring.ts +99 -1
- package/Types/Runbook/RunbookStep.ts +15 -0
- package/Types/Runner/RunnerLiveStatus.ts +114 -0
- package/Types/StatusPage/StatusPageReport.ts +11 -0
- package/Types/StatusPage/StatusPageReportPeriodType.ts +21 -0
- package/UI/Components/Detail/Detail.tsx +155 -0
- package/UI/Components/ModelTable/CustomFieldColumns.tsx +2 -6
- package/UI/Components/ModelTable/useCustomFieldColumns.ts +21 -2
- package/UI/Utils/ModelAPI/ProjectUsersModelAPI.ts +305 -0
- package/UI/Utils/TeamMembersByUser.ts +302 -0
- package/Utils/StatusPage/ReportPeriod.ts +236 -0
- package/Utils/Telemetry/HostIpAddresses.ts +98 -0
- package/build/dist/Models/DatabaseModels/Host.js +12 -4
- package/build/dist/Models/DatabaseModels/Host.js.map +1 -1
- package/build/dist/Models/DatabaseModels/KubernetesContainer.js +4 -4
- package/build/dist/Models/DatabaseModels/KubernetesContainer.js.map +1 -1
- package/build/dist/Models/DatabaseModels/KubernetesResource.js +7 -7
- package/build/dist/Models/DatabaseModels/KubernetesResource.js.map +1 -1
- package/build/dist/Models/DatabaseModels/OnCallDutyPolicyScheduleLayer.js +16 -4
- package/build/dist/Models/DatabaseModels/OnCallDutyPolicyScheduleLayer.js.map +1 -1
- package/build/dist/Models/DatabaseModels/PodmanResource.js +6 -6
- package/build/dist/Models/DatabaseModels/PodmanResource.js.map +1 -1
- package/build/dist/Models/DatabaseModels/ServiceLevelObjective.js +87 -0
- package/build/dist/Models/DatabaseModels/ServiceLevelObjective.js.map +1 -1
- package/build/dist/Models/DatabaseModels/StatusPage.js +98 -0
- package/build/dist/Models/DatabaseModels/StatusPage.js.map +1 -1
- package/build/dist/Server/API/TeamMemberAPI.js +57 -2
- package/build/dist/Server/API/TeamMemberAPI.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/LocalMigrationGenerationDataSource.js +11 -1
- package/build/dist/Server/Infrastructure/Postgres/LocalMigrationGenerationDataSource.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785900000000-AddSloMonitorLabelRule.js +45 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785900000000-AddSloMonitorLabelRule.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785915638551-IncreaseHostIpAddressesLength.js +31 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785915638551-IncreaseHostIpAddressesLength.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785930000000-WidenInventoryResourceNameColumns.js +90 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785930000000-WidenInventoryResourceNameColumns.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785930709405-AddStatusPageReportPeriod.js +14 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785930709405-AddStatusPageReportPeriod.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +8 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
- package/build/dist/Server/Middleware/ProjectAuthorization.js +59 -4
- package/build/dist/Server/Middleware/ProjectAuthorization.js.map +1 -1
- package/build/dist/Server/Services/BillingService.js +466 -36
- package/build/dist/Server/Services/BillingService.js.map +1 -1
- package/build/dist/Server/Services/CephClusterService.js +42 -5
- package/build/dist/Server/Services/CephClusterService.js.map +1 -1
- package/build/dist/Server/Services/CephResourceService.js +28 -3
- package/build/dist/Server/Services/CephResourceService.js.map +1 -1
- package/build/dist/Server/Services/CloudResourceService.js +42 -5
- package/build/dist/Server/Services/CloudResourceService.js.map +1 -1
- package/build/dist/Server/Services/DatabaseService.js +69 -2
- package/build/dist/Server/Services/DatabaseService.js.map +1 -1
- package/build/dist/Server/Services/DockerHostService.js +42 -5
- package/build/dist/Server/Services/DockerHostService.js.map +1 -1
- package/build/dist/Server/Services/DockerResourceService.js +34 -4
- package/build/dist/Server/Services/DockerResourceService.js.map +1 -1
- package/build/dist/Server/Services/DockerSwarmClusterService.js +43 -5
- package/build/dist/Server/Services/DockerSwarmClusterService.js.map +1 -1
- package/build/dist/Server/Services/DockerSwarmResourceService.js +28 -3
- package/build/dist/Server/Services/DockerSwarmResourceService.js.map +1 -1
- package/build/dist/Server/Services/HostService.js +43 -5
- package/build/dist/Server/Services/HostService.js.map +1 -1
- package/build/dist/Server/Services/IoTDeviceService.js +1 -8
- package/build/dist/Server/Services/IoTDeviceService.js.map +1 -1
- package/build/dist/Server/Services/IoTFleetService.js +42 -5
- package/build/dist/Server/Services/IoTFleetService.js.map +1 -1
- package/build/dist/Server/Services/KubernetesClusterService.js +43 -5
- package/build/dist/Server/Services/KubernetesClusterService.js.map +1 -1
- package/build/dist/Server/Services/KubernetesContainerService.js +29 -3
- package/build/dist/Server/Services/KubernetesContainerService.js.map +1 -1
- package/build/dist/Server/Services/KubernetesResourceService.js +32 -4
- package/build/dist/Server/Services/KubernetesResourceService.js.map +1 -1
- package/build/dist/Server/Services/LabelService.js +92 -0
- package/build/dist/Server/Services/LabelService.js.map +1 -1
- package/build/dist/Server/Services/LlmProviderService.js +61 -8
- package/build/dist/Server/Services/LlmProviderService.js.map +1 -1
- package/build/dist/Server/Services/MonitorService.js +47 -0
- package/build/dist/Server/Services/MonitorService.js.map +1 -1
- package/build/dist/Server/Services/PodmanHostService.js +42 -5
- package/build/dist/Server/Services/PodmanHostService.js.map +1 -1
- package/build/dist/Server/Services/PodmanResourceService.js +34 -4
- package/build/dist/Server/Services/PodmanResourceService.js.map +1 -1
- package/build/dist/Server/Services/ProjectService.js +135 -21
- package/build/dist/Server/Services/ProjectService.js.map +1 -1
- package/build/dist/Server/Services/ProxmoxClusterService.js +42 -5
- package/build/dist/Server/Services/ProxmoxClusterService.js.map +1 -1
- package/build/dist/Server/Services/ProxmoxResourceService.js +28 -3
- package/build/dist/Server/Services/ProxmoxResourceService.js.map +1 -1
- package/build/dist/Server/Services/RumApplicationService.js +43 -5
- package/build/dist/Server/Services/RumApplicationService.js.map +1 -1
- package/build/dist/Server/Services/RunnerService.js +1 -6
- package/build/dist/Server/Services/RunnerService.js.map +1 -1
- package/build/dist/Server/Services/ServerlessFunctionService.js +43 -5
- package/build/dist/Server/Services/ServerlessFunctionService.js.map +1 -1
- package/build/dist/Server/Services/ServiceLevelObjectiveMonitorRuleEngineService.js +347 -0
- package/build/dist/Server/Services/ServiceLevelObjectiveMonitorRuleEngineService.js.map +1 -0
- package/build/dist/Server/Services/ServiceLevelObjectiveService.js +33 -1
- package/build/dist/Server/Services/ServiceLevelObjectiveService.js.map +1 -1
- package/build/dist/Server/Services/StatusPageService.js +69 -46
- package/build/dist/Server/Services/StatusPageService.js.map +1 -1
- package/build/dist/Server/Services/StatusPageSubscriberNotificationTemplateService.js +17 -1
- package/build/dist/Server/Services/StatusPageSubscriberNotificationTemplateService.js.map +1 -1
- package/build/dist/Server/Services/StatusPageSubscriberService.js +3 -0
- package/build/dist/Server/Services/StatusPageSubscriberService.js.map +1 -1
- package/build/dist/Server/Types/Database/JSONColumnQuery.js +402 -0
- package/build/dist/Server/Types/Database/JSONColumnQuery.js.map +1 -0
- package/build/dist/Server/Types/Database/Permissions/PublicPermission.js +9 -2
- package/build/dist/Server/Types/Database/Permissions/PublicPermission.js.map +1 -1
- package/build/dist/Server/Types/Database/QueryHelper.js +17 -27
- package/build/dist/Server/Types/Database/QueryHelper.js.map +1 -1
- package/build/dist/Server/Utils/Database/TruncateColumnValue.js +32 -0
- package/build/dist/Server/Utils/Database/TruncateColumnValue.js.map +1 -0
- package/build/dist/Server/Utils/Monitor/Criteria/CompareCriteria.js +11 -0
- package/build/dist/Server/Utils/Monitor/Criteria/CompareCriteria.js.map +1 -1
- package/build/dist/Types/CustomField/CustomFieldDefinition.js +2 -0
- package/build/dist/Types/CustomField/CustomFieldDefinition.js.map +1 -0
- package/build/dist/Types/Date.js +68 -0
- package/build/dist/Types/Date.js.map +1 -1
- package/build/dist/Types/Events/Recurring.js +63 -0
- package/build/dist/Types/Events/Recurring.js.map +1 -1
- package/build/dist/Types/Runbook/RunbookStep.js.map +1 -1
- package/build/dist/Types/Runner/RunnerLiveStatus.js +69 -0
- package/build/dist/Types/Runner/RunnerLiveStatus.js.map +1 -0
- package/build/dist/Types/StatusPage/StatusPageReportPeriodType.js +22 -0
- package/build/dist/Types/StatusPage/StatusPageReportPeriodType.js.map +1 -0
- package/build/dist/UI/Components/Detail/Detail.js +97 -0
- package/build/dist/UI/Components/Detail/Detail.js.map +1 -1
- package/build/dist/UI/Components/ModelTable/CustomFieldColumns.js.map +1 -1
- package/build/dist/UI/Components/ModelTable/useCustomFieldColumns.js +18 -2
- package/build/dist/UI/Components/ModelTable/useCustomFieldColumns.js.map +1 -1
- package/build/dist/UI/Utils/ModelAPI/ProjectUsersModelAPI.js +213 -0
- package/build/dist/UI/Utils/ModelAPI/ProjectUsersModelAPI.js.map +1 -0
- package/build/dist/UI/Utils/TeamMembersByUser.js +195 -0
- package/build/dist/UI/Utils/TeamMembersByUser.js.map +1 -0
- package/build/dist/Utils/StatusPage/ReportPeriod.js +131 -0
- package/build/dist/Utils/StatusPage/ReportPeriod.js.map +1 -0
- package/build/dist/Utils/Telemetry/HostIpAddresses.js +82 -0
- package/build/dist/Utils/Telemetry/HostIpAddresses.js.map +1 -0
- package/jest.config.json +2 -0
- package/package.json +1 -1
- 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
|
+
});
|