@oneuptime/common 12.0.12 → 12.0.13
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/DatabaseBaseModel/DatabaseBaseModel.ts +32 -5
- package/Models/DatabaseModels/NetworkDevice.ts +64 -0
- package/Models/DatabaseModels/StatusPage.ts +59 -0
- package/Models/DatabaseModels/StatusPageOidc.ts +2 -0
- package/Models/DatabaseModels/WorkspaceProjectAuthToken.ts +19 -2
- package/Server/API/AIChatAPI.ts +55 -56
- package/Server/API/CommonAPI.ts +89 -0
- package/Server/API/MicrosoftTeamsAPI.ts +20 -9
- package/Server/API/SlackAPI.ts +13 -10
- package/Server/API/StatusPageAPI.ts +2197 -2128
- package/Server/API/TelemetryAPI.ts +72 -3
- package/Server/Infrastructure/Postgres/SchemaMigrations/1787500000000-AddEnableSearchEngineIndexingToStatusPage.ts +30 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1787600000000-AddNetworkDeviceReachabilityColumns.ts +47 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +4 -0
- package/Server/Middleware/UserAuthorization.ts +11 -2
- package/Server/Services/ApiKeyPermissionService.ts +116 -2
- package/Server/Services/DatabaseService.ts +17 -6
- package/Server/Services/LogAggregationService.ts +34 -0
- package/Server/Services/LogService.ts +21 -0
- package/Server/Services/NetworkSiteService.ts +12 -0
- package/Server/Services/SpanService.ts +21 -0
- package/Server/Services/TraceAggregationService.ts +15 -0
- package/Server/Services/WorkspaceNotificationRuleService.ts +172 -220
- package/Server/Types/AnalyticsDatabase/ModelPermission.ts +9 -0
- package/Server/Types/Database/Permissions/AccessControlPermission.ts +47 -16
- package/Server/Utils/APIKey/AccessPermission.ts +16 -40
- package/Server/Utils/AnalyticsDatabase/StatementGenerator.ts +144 -0
- package/Server/Utils/LogRedaction.ts +576 -0
- package/Server/Utils/Logger.ts +123 -46
- package/Server/Utils/Monitor/Criteria/SSLMonitorCriteria.ts +94 -23
- package/Server/Utils/Monitor/MonitorResource.ts +29 -9
- package/Server/Utils/Monitor/NetworkInventoryUtil.ts +28 -7
- package/Server/Utils/SessionReplay/SessionReplayGateCacheStore.ts +56 -10
- package/Server/Utils/StartServer.ts +30 -9
- package/Server/Utils/StatusPageSearchEngineIndexing.ts +33 -0
- package/Server/Utils/Telemetry/ResourceEntityFilter.ts +441 -0
- package/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.ts +488 -46
- package/Tests/App/Dashboard/MonitorTypePicker.test.tsx +300 -0
- package/Tests/App/StatusPage/StatusPageIndexPageRobotsMeta.test.ts +130 -0
- package/Tests/Models/DatabaseModels/DatabaseBaseModelToJSONObjectCache.test.ts +180 -0
- package/Tests/Models/StatusPageEnableSearchEngineIndexing.test.ts +157 -0
- package/Tests/Server/API/AIChatCancelAndFeedback.test.ts +23 -1
- package/Tests/Server/API/AIChatRouteAuthorization.test.ts +785 -0
- package/Tests/Server/API/BaseAPIApiKeyAuth.test.ts +21 -8
- package/Tests/Server/API/CommonAPIAuthGuard.test.ts +290 -0
- package/Tests/Server/API/MicrosoftTeamsManifest.test.ts +7 -1
- package/Tests/Server/API/StatusPageOverviewCache.test.ts +474 -0
- package/Tests/Server/API/StatusPageSeoSearchEngineIndexing.test.ts +189 -0
- package/Tests/Server/Infrastructure/InMemoryTTLCache.test.ts +158 -0
- package/Tests/Server/Infrastructure/Postgres/DeviceRoleAndDeclaredLinkParentMigration.test.ts +9 -60
- package/Tests/Server/Middleware/ProjectAuthorizationApiKeyMiddleware.test.ts +24 -15
- package/Tests/Server/Middleware/UserAuthorization.test.ts +79 -0
- package/Tests/Server/Services/AddNetworkDeviceReachabilityColumnsMigration.test.ts +332 -0
- package/Tests/Server/Services/ApiKeyPermissionService.test.ts +347 -0
- package/Tests/Server/Services/DatabaseServiceUpdateDebugLogging.test.ts +282 -0
- package/Tests/Server/Services/NetworkSiteService.test.ts +56 -2
- package/Tests/Server/Services/TelemetryResourceFacetFilters.test.ts +267 -0
- package/Tests/Server/Services/WorkspaceNotificationRuleTestRuleChannels.test.ts +702 -0
- package/Tests/Server/Types/Database/Permissions/AccessControlPermission.test.ts +165 -0
- package/Tests/Server/Utils/APIKey/AccessPermission.test.ts +48 -36
- package/Tests/Server/Utils/AnalyticsDatabase/StatementGenerator.test.ts +213 -0
- package/Tests/Server/Utils/LogRedaction.test.ts +415 -0
- package/Tests/Server/Utils/LoggerCredentialLeak.test.ts +245 -0
- package/Tests/Server/Utils/Monitor/Criteria/SSLMonitorCriteria.test.ts +374 -4
- package/Tests/Server/Utils/Monitor/MonitorResourceIngestedStepSelection.test.ts +455 -0
- package/Tests/Server/Utils/Monitor/NetworkDeviceReachabilityRoundTrip.test.ts +369 -0
- package/Tests/Server/Utils/Monitor/NetworkInventoryUtil.test.ts +86 -8
- package/Tests/Server/Utils/PrivacyFilterUtil.test.ts +64 -0
- package/Tests/Server/Utils/SessionReplay/SessionReplayGateCacheStore.test.ts +207 -0
- package/Tests/Server/Utils/StartServerBodyVerify.test.ts +335 -0
- package/Tests/Server/Utils/StatusPageSearchEngineIndexing.test.ts +60 -0
- package/Tests/Server/Utils/Telemetry/ResourceEntityFilter.test.ts +435 -0
- package/Tests/Server/Utils/Workspace/MicrosoftTeamsChannelSend.test.ts +607 -62
- package/Tests/Server/Utils/Workspace/MicrosoftTeamsInstalledTeamIdSpace.test.ts +778 -0
- package/Tests/Server/Utils/Workspace/MicrosoftTeamsTeamInstalls.test.ts +44 -10
- package/Tests/Types/API/HostnameFromAuthority.test.ts +164 -0
- package/Tests/Types/Database/AccessControl/ColumnAccessControlCache.test.ts +344 -0
- package/Tests/Types/Database/DatabasePropertyFindOperator.test.ts +518 -0
- package/Tests/Types/Database/TableColumnMetadataCache.test.ts +286 -0
- package/Tests/Types/Monitor/MonitorStep.test.ts +100 -0
- package/Tests/Types/Monitor/MonitorTypeKeywords.test.ts +360 -0
- package/Tests/Types/StatusPage/SearchEngineIndexing.test.ts +86 -0
- package/Tests/Types/Telemetry/ResourceEntityFacet.test.ts +254 -0
- package/Tests/UI/Components/CardSelect.test.tsx +1021 -0
- package/Tests/UI/Components/MasterPage.test.tsx +36 -1
- package/Tests/Utils/Monitor/MonitorMetricType.test.ts +66 -1
- package/Tests/Utils/Monitor/NetworkTopologyUtil.test.ts +115 -6
- package/Tests/Utils/NetworkDevice/DeviceReachabilityUtil.test.ts +610 -0
- package/Tests/Utils/NetworkSite/SiteStatusRollupUtil.test.ts +114 -45
- package/Types/API/Hostname.ts +79 -0
- package/Types/Database/AccessControl/ColumnAccessControl.ts +33 -11
- package/Types/Database/AccessControl/ColumnBillingAccessControl.ts +33 -11
- package/Types/Database/DatabaseProperty.ts +38 -0
- package/Types/Database/TableColumn.ts +34 -7
- package/Types/Events/Recurring.ts +16 -1
- package/Types/Monitor/MonitorStep.ts +13 -1
- package/Types/Monitor/MonitorType.ts +345 -40
- package/Types/Monitor/SSLMonitor/SslMonitorResponse.ts +43 -0
- package/Types/StatusPage/SearchEngineIndexing.ts +61 -0
- package/Types/Telemetry/ResourceEntityFacet.ts +211 -0
- package/UI/Components/CardSelect/CardSelect.tsx +714 -105
- package/UI/Components/EmptyState/EmptyState.tsx +8 -1
- package/UI/Components/Footer/Footer.tsx +32 -6
- package/UI/Components/Forms/Fields/FormField.tsx +3 -0
- package/UI/Components/Forms/Types/Field.ts +8 -0
- package/UI/Components/Loader/PageLoader.tsx +29 -8
- package/UI/Components/LogsViewer/components/LogsAnalyticsView.tsx +26 -4
- package/UI/Components/Markdown.tsx/LazyMarkdownViewer.tsx +17 -1
- package/UI/Components/MasterPage/MasterPage.tsx +24 -11
- package/Utils/Monitor/MonitorMetricType.ts +16 -1
- package/Utils/Monitor/NetworkTopologyUtil.ts +60 -8
- package/Utils/NetworkDevice/DeviceReachabilityUtil.ts +262 -0
- package/Utils/NetworkSite/SiteStatusRollupUtil.ts +41 -24
- package/Utils/Telemetry/CrossSignalScope.ts +75 -4
- package/build/dist/Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel.js +20 -4
- package/build/dist/Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel.js.map +1 -1
- package/build/dist/Models/DatabaseModels/NetworkDevice.js +66 -0
- package/build/dist/Models/DatabaseModels/NetworkDevice.js.map +1 -1
- package/build/dist/Models/DatabaseModels/StatusPage.js +60 -0
- package/build/dist/Models/DatabaseModels/StatusPage.js.map +1 -1
- package/build/dist/Models/DatabaseModels/StatusPageOidc.js +2 -0
- package/build/dist/Models/DatabaseModels/StatusPageOidc.js.map +1 -1
- package/build/dist/Models/DatabaseModels/WorkspaceProjectAuthToken.js.map +1 -1
- package/build/dist/Server/API/AIChatAPI.js +48 -35
- package/build/dist/Server/API/AIChatAPI.js.map +1 -1
- package/build/dist/Server/API/CommonAPI.js +59 -0
- package/build/dist/Server/API/CommonAPI.js.map +1 -1
- package/build/dist/Server/API/MicrosoftTeamsAPI.js +20 -6
- package/build/dist/Server/API/MicrosoftTeamsAPI.js.map +1 -1
- package/build/dist/Server/API/SlackAPI.js +13 -10
- package/build/dist/Server/API/SlackAPI.js.map +1 -1
- package/build/dist/Server/API/StatusPageAPI.js +745 -673
- package/build/dist/Server/API/StatusPageAPI.js.map +1 -1
- package/build/dist/Server/API/TelemetryAPI.js +34 -3
- package/build/dist/Server/API/TelemetryAPI.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787500000000-AddEnableSearchEngineIndexingToStatusPage.js +23 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787500000000-AddEnableSearchEngineIndexingToStatusPage.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787600000000-AddNetworkDeviceReachabilityColumns.js +34 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787600000000-AddNetworkDeviceReachabilityColumns.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +4 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
- package/build/dist/Server/Middleware/UserAuthorization.js +6 -2
- package/build/dist/Server/Middleware/UserAuthorization.js.map +1 -1
- package/build/dist/Server/Services/ApiKeyPermissionService.js +90 -1
- package/build/dist/Server/Services/ApiKeyPermissionService.js.map +1 -1
- package/build/dist/Server/Services/DatabaseService.js +15 -6
- package/build/dist/Server/Services/DatabaseService.js.map +1 -1
- package/build/dist/Server/Services/LogAggregationService.js +2 -0
- package/build/dist/Server/Services/LogAggregationService.js.map +1 -1
- package/build/dist/Server/Services/LogService.js +16 -0
- package/build/dist/Server/Services/LogService.js.map +1 -1
- package/build/dist/Server/Services/NetworkSiteService.js +12 -0
- package/build/dist/Server/Services/NetworkSiteService.js.map +1 -1
- package/build/dist/Server/Services/SpanService.js +16 -0
- package/build/dist/Server/Services/SpanService.js.map +1 -1
- package/build/dist/Server/Services/TraceAggregationService.js +2 -0
- package/build/dist/Server/Services/TraceAggregationService.js.map +1 -1
- package/build/dist/Server/Services/WorkspaceNotificationRuleService.js +137 -164
- package/build/dist/Server/Services/WorkspaceNotificationRuleService.js.map +1 -1
- package/build/dist/Server/Types/AnalyticsDatabase/ModelPermission.js +9 -0
- package/build/dist/Server/Types/AnalyticsDatabase/ModelPermission.js.map +1 -1
- package/build/dist/Server/Types/Database/Permissions/AccessControlPermission.js +21 -13
- package/build/dist/Server/Types/Database/Permissions/AccessControlPermission.js.map +1 -1
- package/build/dist/Server/Utils/APIKey/AccessPermission.js +9 -32
- package/build/dist/Server/Utils/APIKey/AccessPermission.js.map +1 -1
- package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js +87 -0
- package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js.map +1 -1
- package/build/dist/Server/Utils/LogRedaction.js +449 -0
- package/build/dist/Server/Utils/LogRedaction.js.map +1 -0
- package/build/dist/Server/Utils/Logger.js +98 -45
- package/build/dist/Server/Utils/Logger.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/Criteria/SSLMonitorCriteria.js +65 -13
- package/build/dist/Server/Utils/Monitor/Criteria/SSLMonitorCriteria.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorResource.js +20 -3
- package/build/dist/Server/Utils/Monitor/MonitorResource.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/NetworkInventoryUtil.js +27 -7
- package/build/dist/Server/Utils/Monitor/NetworkInventoryUtil.js.map +1 -1
- package/build/dist/Server/Utils/SessionReplay/SessionReplayGateCacheStore.js +29 -10
- package/build/dist/Server/Utils/SessionReplay/SessionReplayGateCacheStore.js.map +1 -1
- package/build/dist/Server/Utils/StartServer.js +10 -7
- package/build/dist/Server/Utils/StartServer.js.map +1 -1
- package/build/dist/Server/Utils/StatusPageSearchEngineIndexing.js +24 -0
- package/build/dist/Server/Utils/StatusPageSearchEngineIndexing.js.map +1 -0
- package/build/dist/Server/Utils/Telemetry/ResourceEntityFilter.js +311 -0
- package/build/dist/Server/Utils/Telemetry/ResourceEntityFilter.js.map +1 -0
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js +366 -37
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js.map +1 -1
- package/build/dist/Types/API/Hostname.js +63 -0
- package/build/dist/Types/API/Hostname.js.map +1 -1
- package/build/dist/Types/Database/AccessControl/ColumnAccessControl.js +20 -6
- package/build/dist/Types/Database/AccessControl/ColumnAccessControl.js.map +1 -1
- package/build/dist/Types/Database/AccessControl/ColumnBillingAccessControl.js +20 -6
- package/build/dist/Types/Database/AccessControl/ColumnBillingAccessControl.js.map +1 -1
- package/build/dist/Types/Database/DatabaseProperty.js +38 -0
- package/build/dist/Types/Database/DatabaseProperty.js.map +1 -1
- package/build/dist/Types/Database/TableColumn.js +24 -6
- package/build/dist/Types/Database/TableColumn.js.map +1 -1
- package/build/dist/Types/Events/Recurring.js +13 -1
- package/build/dist/Types/Events/Recurring.js.map +1 -1
- package/build/dist/Types/Monitor/MonitorStep.js +9 -1
- package/build/dist/Types/Monitor/MonitorStep.js.map +1 -1
- package/build/dist/Types/Monitor/MonitorType.js +330 -31
- package/build/dist/Types/Monitor/MonitorType.js.map +1 -1
- package/build/dist/Types/StatusPage/SearchEngineIndexing.js +55 -0
- package/build/dist/Types/StatusPage/SearchEngineIndexing.js.map +1 -0
- package/build/dist/Types/Telemetry/ResourceEntityFacet.js +156 -0
- package/build/dist/Types/Telemetry/ResourceEntityFacet.js.map +1 -0
- package/build/dist/UI/Components/CardSelect/CardSelect.js +360 -40
- package/build/dist/UI/Components/CardSelect/CardSelect.js.map +1 -1
- package/build/dist/UI/Components/EmptyState/EmptyState.js +1 -1
- package/build/dist/UI/Components/EmptyState/EmptyState.js.map +1 -1
- package/build/dist/UI/Components/Footer/Footer.js +7 -4
- package/build/dist/UI/Components/Footer/Footer.js.map +1 -1
- package/build/dist/UI/Components/Forms/Fields/FormField.js +1 -1
- package/build/dist/UI/Components/Forms/Fields/FormField.js.map +1 -1
- package/build/dist/UI/Components/Loader/PageLoader.js +15 -5
- package/build/dist/UI/Components/Loader/PageLoader.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/LogsAnalyticsView.js +16 -3
- package/build/dist/UI/Components/LogsViewer/components/LogsAnalyticsView.js.map +1 -1
- package/build/dist/UI/Components/Markdown.tsx/LazyMarkdownViewer.js +13 -1
- package/build/dist/UI/Components/Markdown.tsx/LazyMarkdownViewer.js.map +1 -1
- package/build/dist/UI/Components/MasterPage/MasterPage.js +10 -2
- package/build/dist/UI/Components/MasterPage/MasterPage.js.map +1 -1
- package/build/dist/Utils/Monitor/MonitorMetricType.js +13 -1
- package/build/dist/Utils/Monitor/MonitorMetricType.js.map +1 -1
- package/build/dist/Utils/Monitor/NetworkTopologyUtil.js +34 -7
- package/build/dist/Utils/Monitor/NetworkTopologyUtil.js.map +1 -1
- package/build/dist/Utils/NetworkDevice/DeviceReachabilityUtil.js +177 -0
- package/build/dist/Utils/NetworkDevice/DeviceReachabilityUtil.js.map +1 -0
- package/build/dist/Utils/NetworkSite/SiteStatusRollupUtil.js +23 -27
- package/build/dist/Utils/NetworkSite/SiteStatusRollupUtil.js.map +1 -1
- package/build/dist/Utils/Telemetry/CrossSignalScope.js +44 -4
- package/build/dist/Utils/Telemetry/CrossSignalScope.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,610 @@
|
|
|
1
|
+
import { describe, expect, test } from "@jest/globals";
|
|
2
|
+
import DeviceReachabilityUtil, {
|
|
3
|
+
DEFAULT_DEVICE_POLLING_INTERVAL_IN_MINUTES,
|
|
4
|
+
DEVICE_MIN_STALE_WINDOW_IN_MINUTES,
|
|
5
|
+
DEVICE_MISSED_POLL_ALLOWANCE,
|
|
6
|
+
DeviceReachabilityInput,
|
|
7
|
+
DeviceReachabilityResult,
|
|
8
|
+
NetworkDeviceReachability,
|
|
9
|
+
} from "../../../Utils/NetworkDevice/DeviceReachabilityUtil";
|
|
10
|
+
|
|
11
|
+
/*
|
|
12
|
+
* DeviceReachabilityUtil is the single rule behind every up/down verdict a
|
|
13
|
+
* NetworkDevice gets: the device list pill, the summary tiles, the device
|
|
14
|
+
* Overview hero, the topology graph, the network map and the site rollup.
|
|
15
|
+
*
|
|
16
|
+
* The rule it replaced was "lastSeenAt newer than a fixed 15 minutes", and
|
|
17
|
+
* the whole point of these tests is to pin the distinction that rule could
|
|
18
|
+
* not make — "the device did not answer" versus "we have not asked". Every
|
|
19
|
+
* case below states which of the two it is about.
|
|
20
|
+
*
|
|
21
|
+
* `now` is passed explicitly rather than faked, because that is how every
|
|
22
|
+
* production caller uses it (the topology builder and the site rollup both
|
|
23
|
+
* thread one shared `now` through a whole graph).
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
const NOW: Date = new Date("2026-08-18T12:00:00.000Z");
|
|
27
|
+
|
|
28
|
+
const MS_PER_MINUTE: number = 60 * 1000;
|
|
29
|
+
|
|
30
|
+
function minutesAgo(minutes: number, extraMs: number = 0): Date {
|
|
31
|
+
return new Date(NOW.getTime() - minutes * MS_PER_MINUTE - extraMs);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function statusOf(device: DeviceReachabilityInput): NetworkDeviceReachability {
|
|
35
|
+
return DeviceReachabilityUtil.getStatus(device, NOW);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function reachabilityOf(
|
|
39
|
+
device: DeviceReachabilityInput,
|
|
40
|
+
): DeviceReachabilityResult {
|
|
41
|
+
return DeviceReachabilityUtil.getReachability(device, NOW);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
describe("the constants the rule is built from", () => {
|
|
45
|
+
test("the default interval mirrors the NetworkDevice column default", () => {
|
|
46
|
+
expect(DEFAULT_DEVICE_POLLING_INTERVAL_IN_MINUTES).toBe(5);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("ten missed polls, floored at an hour", () => {
|
|
50
|
+
expect(DEVICE_MISSED_POLL_ALLOWANCE).toBe(10);
|
|
51
|
+
expect(DEVICE_MIN_STALE_WINDOW_IN_MINUTES).toBe(60);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test("the enum carries the display strings the pills render", () => {
|
|
55
|
+
expect(NetworkDeviceReachability.Up).toBe("Up");
|
|
56
|
+
expect(NetworkDeviceReachability.Down).toBe("Down");
|
|
57
|
+
expect(NetworkDeviceReachability.Pending).toBe("Pending");
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
describe("DeviceReachabilityUtil.getPollingIntervalInMinutes", () => {
|
|
62
|
+
test("keeps a sane configured interval", () => {
|
|
63
|
+
expect(DeviceReachabilityUtil.getPollingIntervalInMinutes(5)).toBe(5);
|
|
64
|
+
expect(DeviceReachabilityUtil.getPollingIntervalInMinutes(30)).toBe(30);
|
|
65
|
+
expect(DeviceReachabilityUtil.getPollingIntervalInMinutes(1440)).toBe(1440);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test("a missing interval falls back to the column default", () => {
|
|
69
|
+
expect(DeviceReachabilityUtil.getPollingIntervalInMinutes(undefined)).toBe(
|
|
70
|
+
5,
|
|
71
|
+
);
|
|
72
|
+
expect(DeviceReachabilityUtil.getPollingIntervalInMinutes(null)).toBe(5);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
/*
|
|
76
|
+
* The same clamp NetworkDeviceService.claimDevicesForPolling applies when
|
|
77
|
+
* it advances nextPollAt. If the two disagreed, the window would be sized
|
|
78
|
+
* against a schedule the scheduler never runs.
|
|
79
|
+
*/
|
|
80
|
+
test("zero, negative and non-finite intervals fall back too", () => {
|
|
81
|
+
expect(DeviceReachabilityUtil.getPollingIntervalInMinutes(0)).toBe(5);
|
|
82
|
+
expect(DeviceReachabilityUtil.getPollingIntervalInMinutes(-15)).toBe(5);
|
|
83
|
+
expect(DeviceReachabilityUtil.getPollingIntervalInMinutes(NaN)).toBe(5);
|
|
84
|
+
expect(
|
|
85
|
+
DeviceReachabilityUtil.getPollingIntervalInMinutes(
|
|
86
|
+
Number.POSITIVE_INFINITY,
|
|
87
|
+
),
|
|
88
|
+
).toBe(5);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test("a sub-minute interval is clamped up to one minute", () => {
|
|
92
|
+
expect(DeviceReachabilityUtil.getPollingIntervalInMinutes(0.5)).toBe(1);
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
describe("DeviceReachabilityUtil.getStaleWindowInMinutes", () => {
|
|
97
|
+
test("short intervals get the one-hour floor, not ten times nothing", () => {
|
|
98
|
+
expect(DeviceReachabilityUtil.getStaleWindowInMinutes(1)).toBe(60);
|
|
99
|
+
expect(DeviceReachabilityUtil.getStaleWindowInMinutes(5)).toBe(60);
|
|
100
|
+
expect(DeviceReachabilityUtil.getStaleWindowInMinutes(6)).toBe(60);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test("beyond six minutes the window scales with the device's own schedule", () => {
|
|
104
|
+
expect(DeviceReachabilityUtil.getStaleWindowInMinutes(10)).toBe(100);
|
|
105
|
+
expect(DeviceReachabilityUtil.getStaleWindowInMinutes(30)).toBe(300);
|
|
106
|
+
expect(DeviceReachabilityUtil.getStaleWindowInMinutes(60)).toBe(600);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
/*
|
|
110
|
+
* The old rule was a flat 15 minutes for every device, which a device
|
|
111
|
+
* polled every 15, 30 or 60 minutes could never satisfy — it was down the
|
|
112
|
+
* moment it was configured.
|
|
113
|
+
*/
|
|
114
|
+
test("a slow-polled device's window always exceeds its own interval", () => {
|
|
115
|
+
for (const interval of [1, 5, 15, 30, 60, 120, 720, 1440]) {
|
|
116
|
+
expect(
|
|
117
|
+
DeviceReachabilityUtil.getStaleWindowInMinutes(interval),
|
|
118
|
+
).toBeGreaterThan(interval);
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
describe("the last poll failed → Down", () => {
|
|
124
|
+
test("isReachable false is Down", () => {
|
|
125
|
+
expect(statusOf({ isReachable: false })).toBe(
|
|
126
|
+
NetworkDeviceReachability.Down,
|
|
127
|
+
);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
/*
|
|
131
|
+
* The verdict does not expire. Nothing since has contradicted it, and
|
|
132
|
+
* inventing an expiry is how "we have not asked lately" became "the
|
|
133
|
+
* device is up" in the other direction.
|
|
134
|
+
*/
|
|
135
|
+
test("Down stands however old the failed poll is", () => {
|
|
136
|
+
expect(
|
|
137
|
+
statusOf({
|
|
138
|
+
isReachable: false,
|
|
139
|
+
lastPolledAt: minutesAgo(1),
|
|
140
|
+
lastSeenAt: minutesAgo(90),
|
|
141
|
+
}),
|
|
142
|
+
).toBe(NetworkDeviceReachability.Down);
|
|
143
|
+
|
|
144
|
+
expect(
|
|
145
|
+
statusOf({
|
|
146
|
+
isReachable: false,
|
|
147
|
+
lastPolledAt: minutesAgo(60 * 24 * 30),
|
|
148
|
+
lastSeenAt: minutesAgo(60 * 24 * 30),
|
|
149
|
+
}),
|
|
150
|
+
).toBe(NetworkDeviceReachability.Down);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
test("a device that answered recently but failed its LAST poll is Down", () => {
|
|
154
|
+
expect(
|
|
155
|
+
statusOf({
|
|
156
|
+
isReachable: false,
|
|
157
|
+
lastPolledAt: minutesAgo(1),
|
|
158
|
+
lastSeenAt: minutesAgo(6),
|
|
159
|
+
pollingIntervalInMinutes: 5,
|
|
160
|
+
}),
|
|
161
|
+
).toBe(NetworkDeviceReachability.Down);
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
describe("the last poll succeeded → Up", () => {
|
|
166
|
+
test("isReachable true, polled just now, is Up", () => {
|
|
167
|
+
expect(
|
|
168
|
+
statusOf({
|
|
169
|
+
isReachable: true,
|
|
170
|
+
lastPolledAt: NOW,
|
|
171
|
+
lastSeenAt: NOW,
|
|
172
|
+
pollingIntervalInMinutes: 5,
|
|
173
|
+
}),
|
|
174
|
+
).toBe(NetworkDeviceReachability.Up);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
/*
|
|
178
|
+
* THE REGRESSION. Issue #3220: UN1234WANRTR01 answered SNMP — its
|
|
179
|
+
* Interfaces tab showed 14 ports up, written by that very walk — but the
|
|
180
|
+
* fleet was big enough that its probe could only get round to it every
|
|
181
|
+
* ~20 minutes, so the last successful poll was 21 minutes old and the old
|
|
182
|
+
* fixed 15-minute freshness window called the device Down.
|
|
183
|
+
*/
|
|
184
|
+
test("issue #3220: a device polled 21 minutes ago on a 5-minute interval is Up", () => {
|
|
185
|
+
expect(
|
|
186
|
+
statusOf({
|
|
187
|
+
isReachable: true,
|
|
188
|
+
lastPolledAt: minutesAgo(21),
|
|
189
|
+
lastSeenAt: minutesAgo(21),
|
|
190
|
+
pollingIntervalInMinutes: 5,
|
|
191
|
+
}),
|
|
192
|
+
).toBe(NetworkDeviceReachability.Up);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
test("issue #3220, generalised: no lag short of the stale window flips a good poll to Down", () => {
|
|
196
|
+
for (const lagInMinutes of [16, 21, 30, 45, 59]) {
|
|
197
|
+
expect(
|
|
198
|
+
statusOf({
|
|
199
|
+
isReachable: true,
|
|
200
|
+
lastPolledAt: minutesAgo(lagInMinutes),
|
|
201
|
+
lastSeenAt: minutesAgo(lagInMinutes),
|
|
202
|
+
pollingIntervalInMinutes: 5,
|
|
203
|
+
}),
|
|
204
|
+
).toBe(NetworkDeviceReachability.Up);
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
/*
|
|
209
|
+
* The other half of the same bug, with no fleet-size excuse needed: a
|
|
210
|
+
* device configured to be polled every 30 minutes could not be inside a
|
|
211
|
+
* 15-minute window at any point in its cycle.
|
|
212
|
+
*/
|
|
213
|
+
test("a device on a 30-minute interval is Up right before its next poll", () => {
|
|
214
|
+
expect(
|
|
215
|
+
statusOf({
|
|
216
|
+
isReachable: true,
|
|
217
|
+
lastPolledAt: minutesAgo(29),
|
|
218
|
+
lastSeenAt: minutesAgo(29),
|
|
219
|
+
pollingIntervalInMinutes: 30,
|
|
220
|
+
}),
|
|
221
|
+
).toBe(NetworkDeviceReachability.Up);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
test("a device on a 60-minute interval is Up right before its next poll", () => {
|
|
225
|
+
expect(
|
|
226
|
+
statusOf({
|
|
227
|
+
isReachable: true,
|
|
228
|
+
lastPolledAt: minutesAgo(59),
|
|
229
|
+
lastSeenAt: minutesAgo(59),
|
|
230
|
+
pollingIntervalInMinutes: 60,
|
|
231
|
+
}),
|
|
232
|
+
).toBe(NetworkDeviceReachability.Up);
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
test("a clock-skewed future timestamp is Up, not an error", () => {
|
|
236
|
+
expect(
|
|
237
|
+
statusOf({
|
|
238
|
+
isReachable: true,
|
|
239
|
+
lastPolledAt: minutesAgo(-5),
|
|
240
|
+
lastSeenAt: minutesAgo(-5),
|
|
241
|
+
}),
|
|
242
|
+
).toBe(NetworkDeviceReachability.Up);
|
|
243
|
+
});
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
/*
|
|
247
|
+
* Staleness ANNOTATES the verdict; it never replaces it.
|
|
248
|
+
*
|
|
249
|
+
* Two reasons, and the tests below pin both. Turning "nothing has polled
|
|
250
|
+
* this in a while" into "the device is down" is the exact inference this
|
|
251
|
+
* whole change exists to remove — doing it again at an hour instead of
|
|
252
|
+
* fifteen minutes would just move the bug. And the window is per-device
|
|
253
|
+
* (derived from each row's own interval), so a status that depended on it
|
|
254
|
+
* could never be expressed as a SQL filter — the device list's summary
|
|
255
|
+
* counts and its Status chip run in the database over `isReachable`, and a
|
|
256
|
+
* pill they cannot reproduce is a pill that contradicts them on screen.
|
|
257
|
+
*/
|
|
258
|
+
describe("the polling pipeline stopped → still Up, but flagged stale", () => {
|
|
259
|
+
test("a good verdict older than the stale window stays Up and is flagged", () => {
|
|
260
|
+
const result: DeviceReachabilityResult = reachabilityOf({
|
|
261
|
+
isReachable: true,
|
|
262
|
+
lastPolledAt: minutesAgo(61),
|
|
263
|
+
lastSeenAt: minutesAgo(61),
|
|
264
|
+
pollingIntervalInMinutes: 5,
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
expect(result.status).toBe(NetworkDeviceReachability.Up);
|
|
268
|
+
expect(result.isStale).toBe(true);
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
test("the staleness boundary is strict, and moves only isStale", () => {
|
|
272
|
+
expect(
|
|
273
|
+
reachabilityOf({
|
|
274
|
+
isReachable: true,
|
|
275
|
+
lastPolledAt: minutesAgo(60),
|
|
276
|
+
pollingIntervalInMinutes: 5,
|
|
277
|
+
}),
|
|
278
|
+
).toMatchObject({ status: NetworkDeviceReachability.Up, isStale: false });
|
|
279
|
+
|
|
280
|
+
expect(
|
|
281
|
+
reachabilityOf({
|
|
282
|
+
isReachable: true,
|
|
283
|
+
lastPolledAt: minutesAgo(60, 1),
|
|
284
|
+
pollingIntervalInMinutes: 5,
|
|
285
|
+
}),
|
|
286
|
+
).toMatchObject({ status: NetworkDeviceReachability.Up, isStale: true });
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
test("the window follows the device's interval, so a slow device is not flagged early", () => {
|
|
290
|
+
// 30-minute interval -> 300-minute window: four hours is fine.
|
|
291
|
+
expect(
|
|
292
|
+
reachabilityOf({
|
|
293
|
+
isReachable: true,
|
|
294
|
+
lastPolledAt: minutesAgo(240),
|
|
295
|
+
pollingIntervalInMinutes: 30,
|
|
296
|
+
}).isStale,
|
|
297
|
+
).toBe(false);
|
|
298
|
+
|
|
299
|
+
// Six hours is not.
|
|
300
|
+
expect(
|
|
301
|
+
reachabilityOf({
|
|
302
|
+
isReachable: true,
|
|
303
|
+
lastPolledAt: minutesAgo(360),
|
|
304
|
+
pollingIntervalInMinutes: 30,
|
|
305
|
+
}).isStale,
|
|
306
|
+
).toBe(true);
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
/*
|
|
310
|
+
* The invariant that keeps the pill, the summary counts and the Status
|
|
311
|
+
* chip in agreement. The latter two are SQL over `isReachable` alone, so
|
|
312
|
+
* the moment anything else can change `status`, they diverge — which is
|
|
313
|
+
* precisely how a stale fleet came to render 40 red rows under a tile
|
|
314
|
+
* reading "Devices Down 0".
|
|
315
|
+
*/
|
|
316
|
+
test("status is decided by isReachable alone, whatever the timestamps say", () => {
|
|
317
|
+
const ages: Array<number> = [0, 1, 59, 60, 61, 600, 60 * 24 * 30];
|
|
318
|
+
|
|
319
|
+
for (const age of ages) {
|
|
320
|
+
expect(
|
|
321
|
+
statusOf({
|
|
322
|
+
isReachable: true,
|
|
323
|
+
lastPolledAt: minutesAgo(age),
|
|
324
|
+
lastSeenAt: minutesAgo(age),
|
|
325
|
+
pollingIntervalInMinutes: 5,
|
|
326
|
+
}),
|
|
327
|
+
).toBe(NetworkDeviceReachability.Up);
|
|
328
|
+
|
|
329
|
+
expect(
|
|
330
|
+
statusOf({
|
|
331
|
+
isReachable: false,
|
|
332
|
+
lastPolledAt: minutesAgo(age),
|
|
333
|
+
lastSeenAt: minutesAgo(age + 10),
|
|
334
|
+
pollingIntervalInMinutes: 5,
|
|
335
|
+
}),
|
|
336
|
+
).toBe(NetworkDeviceReachability.Down);
|
|
337
|
+
}
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
test("the result names the window it judged against, so the UI can explain itself", () => {
|
|
341
|
+
expect(
|
|
342
|
+
reachabilityOf({ isReachable: true, pollingIntervalInMinutes: 30 })
|
|
343
|
+
.staleWindowInMinutes,
|
|
344
|
+
).toBe(300);
|
|
345
|
+
expect(
|
|
346
|
+
reachabilityOf({ isReachable: true, pollingIntervalInMinutes: 5 })
|
|
347
|
+
.staleWindowInMinutes,
|
|
348
|
+
).toBe(60);
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
/*
|
|
352
|
+
* A failing device is in contact — we are reaching its probe, the probe
|
|
353
|
+
* is reaching the network, the device just is not answering. Measuring
|
|
354
|
+
* staleness from lastSeenAt would call that "out of contact" and hide the
|
|
355
|
+
* real, more specific diagnosis behind a probe-health message.
|
|
356
|
+
*/
|
|
357
|
+
test("a device failing every poll is Down but NOT stale", () => {
|
|
358
|
+
const result: DeviceReachabilityResult = reachabilityOf({
|
|
359
|
+
isReachable: false,
|
|
360
|
+
lastPolledAt: minutesAgo(2),
|
|
361
|
+
lastSeenAt: minutesAgo(600),
|
|
362
|
+
pollingIntervalInMinutes: 5,
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
expect(result.status).toBe(NetworkDeviceReachability.Down);
|
|
366
|
+
expect(result.isStale).toBe(false);
|
|
367
|
+
expect(result.lastContactAt?.getTime()).toBe(minutesAgo(2).getTime());
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
test("a device can be both Down and stale, reported independently", () => {
|
|
371
|
+
const result: DeviceReachabilityResult = reachabilityOf({
|
|
372
|
+
isReachable: false,
|
|
373
|
+
lastPolledAt: minutesAgo(600),
|
|
374
|
+
pollingIntervalInMinutes: 5,
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
expect(result.status).toBe(NetworkDeviceReachability.Down);
|
|
378
|
+
expect(result.isStale).toBe(true);
|
|
379
|
+
});
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
describe("never polled → Pending", () => {
|
|
383
|
+
test("a device with no poll history at all is Pending", () => {
|
|
384
|
+
expect(statusOf({})).toBe(NetworkDeviceReachability.Pending);
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
test("explicit nulls (raw API payloads) are Pending", () => {
|
|
388
|
+
expect(
|
|
389
|
+
statusOf({
|
|
390
|
+
isReachable: null,
|
|
391
|
+
lastPolledAt: null,
|
|
392
|
+
lastSeenAt: null,
|
|
393
|
+
pollingIntervalInMinutes: null,
|
|
394
|
+
}),
|
|
395
|
+
).toBe(NetworkDeviceReachability.Pending);
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
test("empty-string dates are Pending, not Invalid Date", () => {
|
|
399
|
+
expect(statusOf({ lastPolledAt: "", lastSeenAt: "" })).toBe(
|
|
400
|
+
NetworkDeviceReachability.Pending,
|
|
401
|
+
);
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
test("Pending is never reported as stale — there is nothing to have gone stale", () => {
|
|
405
|
+
const result: DeviceReachabilityResult = reachabilityOf({});
|
|
406
|
+
|
|
407
|
+
expect(result.isStale).toBe(false);
|
|
408
|
+
expect(result.lastContactAt).toBeNull();
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
/*
|
|
412
|
+
* Polled repeatedly and never once answered. That is a device that is
|
|
413
|
+
* down, not a device waiting to be set up — calling it Pending would hide
|
|
414
|
+
* it from every "what is broken" view on the product.
|
|
415
|
+
*/
|
|
416
|
+
test("polled but never answered is Down, not Pending", () => {
|
|
417
|
+
expect(
|
|
418
|
+
statusOf({
|
|
419
|
+
lastPolledAt: minutesAgo(2),
|
|
420
|
+
lastSeenAt: undefined,
|
|
421
|
+
}),
|
|
422
|
+
).toBe(NetworkDeviceReachability.Down);
|
|
423
|
+
});
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
/*
|
|
427
|
+
* Rows written before the isReachable column existed carry only lastSeenAt
|
|
428
|
+
* until their first walk after the upgrade rewrites them. The upgrade
|
|
429
|
+
* migration backfills them, so this is a narrow window — but a device must
|
|
430
|
+
* not read Pending (and vanish from the Down list) during it.
|
|
431
|
+
*/
|
|
432
|
+
describe("rows with no recorded outcome fall back to freshness", () => {
|
|
433
|
+
test("a legacy row seen recently is Up", () => {
|
|
434
|
+
expect(statusOf({ lastSeenAt: minutesAgo(5) })).toBe(
|
|
435
|
+
NetworkDeviceReachability.Up,
|
|
436
|
+
);
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
test("a legacy row is judged against the same generous window", () => {
|
|
440
|
+
expect(statusOf({ lastSeenAt: minutesAgo(21) })).toBe(
|
|
441
|
+
NetworkDeviceReachability.Up,
|
|
442
|
+
);
|
|
443
|
+
expect(statusOf({ lastSeenAt: minutesAgo(61) })).toBe(
|
|
444
|
+
NetworkDeviceReachability.Down,
|
|
445
|
+
);
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
test("a legacy row's interval still sizes its window", () => {
|
|
449
|
+
expect(
|
|
450
|
+
statusOf({ lastSeenAt: minutesAgo(200), pollingIntervalInMinutes: 30 }),
|
|
451
|
+
).toBe(NetworkDeviceReachability.Up);
|
|
452
|
+
});
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
describe("date parsing", () => {
|
|
456
|
+
test("accepts ISO strings, the form the API serializes", () => {
|
|
457
|
+
expect(
|
|
458
|
+
statusOf({
|
|
459
|
+
isReachable: true,
|
|
460
|
+
lastPolledAt: "2026-08-18T11:39:00.000Z",
|
|
461
|
+
lastSeenAt: "2026-08-18T11:39:00.000Z",
|
|
462
|
+
}),
|
|
463
|
+
).toBe(NetworkDeviceReachability.Up);
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
test("accepts Date objects", () => {
|
|
467
|
+
expect(statusOf({ isReachable: true, lastPolledAt: new Date(NOW) })).toBe(
|
|
468
|
+
NetworkDeviceReachability.Up,
|
|
469
|
+
);
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
/*
|
|
473
|
+
* An Invalid Date compares false against every window, which would
|
|
474
|
+
* silently read as "fresh". Treating it as "not set" makes the outcome
|
|
475
|
+
* column the only thing deciding, which is at least honest.
|
|
476
|
+
*/
|
|
477
|
+
test("an unparseable date reads as not-set rather than as fresh", () => {
|
|
478
|
+
const result: DeviceReachabilityResult = reachabilityOf({
|
|
479
|
+
lastSeenAt: "not-a-date-at-all",
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
expect(result.lastContactAt).toBeNull();
|
|
483
|
+
expect(result.status).toBe(NetworkDeviceReachability.Pending);
|
|
484
|
+
});
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
describe("lastContactAt is the newer of the two timestamps", () => {
|
|
488
|
+
test("the later attempt wins over the earlier success", () => {
|
|
489
|
+
expect(
|
|
490
|
+
reachabilityOf({
|
|
491
|
+
isReachable: false,
|
|
492
|
+
lastPolledAt: minutesAgo(1),
|
|
493
|
+
lastSeenAt: minutesAgo(30),
|
|
494
|
+
}).lastContactAt?.getTime(),
|
|
495
|
+
).toBe(minutesAgo(1).getTime());
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
test("a legacy row with only lastSeenAt uses it", () => {
|
|
499
|
+
expect(
|
|
500
|
+
reachabilityOf({ lastSeenAt: minutesAgo(7) }).lastContactAt?.getTime(),
|
|
501
|
+
).toBe(minutesAgo(7).getTime());
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
test("a device polled but never seen uses the attempt", () => {
|
|
505
|
+
expect(
|
|
506
|
+
reachabilityOf({
|
|
507
|
+
isReachable: false,
|
|
508
|
+
lastPolledAt: minutesAgo(3),
|
|
509
|
+
}).lastContactAt?.getTime(),
|
|
510
|
+
).toBe(minutesAgo(3).getTime());
|
|
511
|
+
});
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
/*
|
|
515
|
+
* The scenario from the issue, at the scale the issue reports it: 980
|
|
516
|
+
* devices behind one probe that can only claim 50 a minute, so the fleet
|
|
517
|
+
* takes ~20 minutes to cycle and every device's last successful poll is
|
|
518
|
+
* uniformly spread across that window. Under the old rule roughly a quarter
|
|
519
|
+
* of a perfectly healthy fleet was red at any instant, which is what "323
|
|
520
|
+
* of 980 Down/Stale" was.
|
|
521
|
+
*/
|
|
522
|
+
describe("issue #3220 at fleet scale", () => {
|
|
523
|
+
function healthyFleetPolledOverMinutes(
|
|
524
|
+
cycleInMinutes: number,
|
|
525
|
+
deviceCount: number,
|
|
526
|
+
): Array<DeviceReachabilityInput> {
|
|
527
|
+
const devices: Array<DeviceReachabilityInput> = [];
|
|
528
|
+
|
|
529
|
+
for (let index: number = 0; index < deviceCount; index++) {
|
|
530
|
+
const ageInMinutes: number = (index / deviceCount) * cycleInMinutes;
|
|
531
|
+
devices.push({
|
|
532
|
+
isReachable: true,
|
|
533
|
+
lastPolledAt: minutesAgo(ageInMinutes),
|
|
534
|
+
lastSeenAt: minutesAgo(ageInMinutes),
|
|
535
|
+
pollingIntervalInMinutes: 5,
|
|
536
|
+
});
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
return devices;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
test("a healthy fleet on a 20-minute cycle reports zero devices down", () => {
|
|
543
|
+
const fleet: Array<DeviceReachabilityInput> = healthyFleetPolledOverMinutes(
|
|
544
|
+
20,
|
|
545
|
+
980,
|
|
546
|
+
);
|
|
547
|
+
|
|
548
|
+
const down: number = fleet.filter(
|
|
549
|
+
(device: DeviceReachabilityInput): boolean => {
|
|
550
|
+
return statusOf(device) === NetworkDeviceReachability.Down;
|
|
551
|
+
},
|
|
552
|
+
).length;
|
|
553
|
+
|
|
554
|
+
expect(down).toBe(0);
|
|
555
|
+
});
|
|
556
|
+
|
|
557
|
+
test("genuinely unreachable devices in that fleet are still counted", () => {
|
|
558
|
+
const fleet: Array<DeviceReachabilityInput> = healthyFleetPolledOverMinutes(
|
|
559
|
+
20,
|
|
560
|
+
100,
|
|
561
|
+
);
|
|
562
|
+
|
|
563
|
+
// Three of them failed their last walk.
|
|
564
|
+
fleet[10]!.isReachable = false;
|
|
565
|
+
fleet[40]!.isReachable = false;
|
|
566
|
+
fleet[90]!.isReachable = false;
|
|
567
|
+
|
|
568
|
+
const down: number = fleet.filter(
|
|
569
|
+
(device: DeviceReachabilityInput): boolean => {
|
|
570
|
+
return statusOf(device) === NetworkDeviceReachability.Down;
|
|
571
|
+
},
|
|
572
|
+
).length;
|
|
573
|
+
|
|
574
|
+
expect(down).toBe(3);
|
|
575
|
+
});
|
|
576
|
+
|
|
577
|
+
/*
|
|
578
|
+
* A probe that has stopped entirely. The fleet keeps its last known
|
|
579
|
+
* verdicts — inventing an outage nobody observed is the failure mode this
|
|
580
|
+
* change exists to remove — but every device is flagged stale, which is
|
|
581
|
+
* the signal that says "check the probe" rather than "check 50 devices".
|
|
582
|
+
*/
|
|
583
|
+
test("a fleet whose probe has stopped is flagged stale, not declared down", () => {
|
|
584
|
+
const fleet: Array<DeviceReachabilityInput> = healthyFleetPolledOverMinutes(
|
|
585
|
+
20,
|
|
586
|
+
50,
|
|
587
|
+
).map((device: DeviceReachabilityInput): DeviceReachabilityInput => {
|
|
588
|
+
// Nothing has been polled for three hours.
|
|
589
|
+
return {
|
|
590
|
+
...device,
|
|
591
|
+
lastPolledAt: minutesAgo(180),
|
|
592
|
+
lastSeenAt: minutesAgo(180),
|
|
593
|
+
};
|
|
594
|
+
});
|
|
595
|
+
|
|
596
|
+
const down: number = fleet.filter(
|
|
597
|
+
(device: DeviceReachabilityInput): boolean => {
|
|
598
|
+
return statusOf(device) === NetworkDeviceReachability.Down;
|
|
599
|
+
},
|
|
600
|
+
).length;
|
|
601
|
+
const stale: number = fleet.filter(
|
|
602
|
+
(device: DeviceReachabilityInput): boolean => {
|
|
603
|
+
return reachabilityOf(device).isStale;
|
|
604
|
+
},
|
|
605
|
+
).length;
|
|
606
|
+
|
|
607
|
+
expect(down).toBe(0);
|
|
608
|
+
expect(stale).toBe(50);
|
|
609
|
+
});
|
|
610
|
+
});
|