@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
@@ -0,0 +1,387 @@
1
+ import CephClusterService from "../../../Server/Services/CephClusterService";
2
+ import CloudResourceService from "../../../Server/Services/CloudResourceService";
3
+ import DockerHostService from "../../../Server/Services/DockerHostService";
4
+ import DockerSwarmClusterService from "../../../Server/Services/DockerSwarmClusterService";
5
+ import GlobalCache from "../../../Server/Infrastructure/GlobalCache";
6
+ import HostService from "../../../Server/Services/HostService";
7
+ import IoTFleetService from "../../../Server/Services/IoTFleetService";
8
+ import KubernetesClusterService from "../../../Server/Services/KubernetesClusterService";
9
+ import PodmanHostService from "../../../Server/Services/PodmanHostService";
10
+ import ProxmoxClusterService from "../../../Server/Services/ProxmoxClusterService";
11
+ import RumApplicationService from "../../../Server/Services/RumApplicationService";
12
+ import ServerlessFunctionService from "../../../Server/Services/ServerlessFunctionService";
13
+ import ObjectID from "../../../Types/ObjectID";
14
+ import {
15
+ afterEach,
16
+ beforeEach,
17
+ describe,
18
+ expect,
19
+ jest,
20
+ test,
21
+ } from "@jest/globals";
22
+
23
+ /*
24
+ * Issue #3006 was reported against Host, but HostService.updateLastSeen was
25
+ * never the only service shaped that way. Ten siblings write the same two
26
+ * liveness columns (lastSeenAt + otelCollectorStatus) in the SAME hook-free
27
+ * UPDATE as a handful of optional columns harvested from OpenTelemetry
28
+ * resource attributes — and each has its own markDisconnected* job that
29
+ * flips the resource to "disconnected" 15 minutes after lastSeenAt stops
30
+ * advancing.
31
+ *
32
+ * The clamp that stops a Postgres rejection now lives inside
33
+ * updateColumnsByIdWithoutHooks (see HookFreeWriteLengthClamp.test.ts), so
34
+ * the statement should not fail in the first place. This suite pins the
35
+ * SECOND, independent guard, per service: if the enriched write fails
36
+ * ANYWAY — for any reason at all, a constraint nobody predicted, a column
37
+ * type nobody clamped — the liveness columns are retried on their own.
38
+ * Enrichment is what gets dropped, never the heartbeat.
39
+ *
40
+ * Also pinned per service: the throttle that keeps the steady state cheap
41
+ * must not swallow the retry, and a Redis outage must fail OPEN. Marking a
42
+ * live resource disconnected because the cache went down would be the same
43
+ * bug wearing a different hat.
44
+ *
45
+ * Everything external is mocked — no Postgres, no Redis.
46
+ */
47
+
48
+ type WriteCall = { id: ObjectID; data: Record<string, unknown> };
49
+
50
+ type ServiceCase = {
51
+ /** Name of the service, used as the describe() title. */
52
+ name: string;
53
+ service: {
54
+ updateLastSeen(id: ObjectID, extra?: never): Promise<void>;
55
+ updateColumnsByIdWithoutHooks(input: {
56
+ id: ObjectID;
57
+ data: unknown;
58
+ }): Promise<void>;
59
+ };
60
+ /** A metadata payload this service accepts, and a different one. */
61
+ extra: Record<string, unknown>;
62
+ otherExtra: Record<string, unknown>;
63
+ /** A string column whose value the collector supplies. */
64
+ oversizedExtra: Record<string, unknown>;
65
+ };
66
+
67
+ /** Longer than any bounded column on any of these models. */
68
+ const OVERSIZED: string = "x".repeat(5000);
69
+
70
+ /* eslint-disable @typescript-eslint/no-explicit-any */
71
+ const SERVICE_CASES: Array<ServiceCase> = [
72
+ {
73
+ name: "DockerHostService",
74
+ service: DockerHostService as any,
75
+ extra: { osType: "linux" },
76
+ otherExtra: { osType: "windows" },
77
+ oversizedExtra: { osVersion: OVERSIZED },
78
+ },
79
+ {
80
+ name: "PodmanHostService",
81
+ service: PodmanHostService as any,
82
+ extra: { osType: "linux" },
83
+ otherExtra: { osType: "windows" },
84
+ oversizedExtra: { osVersion: OVERSIZED },
85
+ },
86
+ {
87
+ name: "KubernetesClusterService",
88
+ service: KubernetesClusterService as any,
89
+ extra: { agentVersion: "1.2.3" },
90
+ otherExtra: { agentVersion: "1.2.4" },
91
+ oversizedExtra: { agentVersion: OVERSIZED },
92
+ },
93
+ {
94
+ name: "ProxmoxClusterService",
95
+ service: ProxmoxClusterService as any,
96
+ extra: { pveVersion: "8.1.4" },
97
+ otherExtra: { pveVersion: "8.2.0" },
98
+ oversizedExtra: { pveVersion: OVERSIZED },
99
+ },
100
+ {
101
+ name: "IoTFleetService",
102
+ service: IoTFleetService as any,
103
+ extra: { agentVersion: "1.2.3" },
104
+ otherExtra: { agentVersion: "1.2.4" },
105
+ oversizedExtra: { agentVersion: OVERSIZED },
106
+ },
107
+ {
108
+ name: "DockerSwarmClusterService",
109
+ service: DockerSwarmClusterService as any,
110
+ extra: { dockerVersion: "25.0.3" },
111
+ otherExtra: { dockerVersion: "26.0.0" },
112
+ oversizedExtra: { swarmId: OVERSIZED },
113
+ },
114
+ {
115
+ name: "CephClusterService",
116
+ service: CephClusterService as any,
117
+ extra: { cephVersion: "18.2.2" },
118
+ otherExtra: { cephVersion: "19.0.0" },
119
+ oversizedExtra: { fsid: OVERSIZED },
120
+ },
121
+ {
122
+ name: "ServerlessFunctionService",
123
+ service: ServerlessFunctionService as any,
124
+ extra: { runtimeName: "nodejs" },
125
+ otherExtra: { runtimeName: "python" },
126
+ oversizedExtra: { functionVersion: OVERSIZED },
127
+ },
128
+ {
129
+ name: "CloudResourceService",
130
+ service: CloudResourceService as any,
131
+ extra: { cloudProvider: "aws" },
132
+ otherExtra: { cloudProvider: "gcp" },
133
+ oversizedExtra: { cloudRegion: OVERSIZED },
134
+ },
135
+ {
136
+ name: "RumApplicationService",
137
+ service: RumApplicationService as any,
138
+ extra: { sdkLanguage: "webjs" },
139
+ otherExtra: { sdkLanguage: "swift" },
140
+ oversizedExtra: { clientType: OVERSIZED },
141
+ },
142
+ {
143
+ /*
144
+ * Host rides the same table so the eleven paths stay pinned together —
145
+ * its own richer regression coverage lives in
146
+ * HostServiceUpdateLastSeen.test.ts.
147
+ */
148
+ name: "HostService",
149
+ service: HostService as any,
150
+ extra: { osType: "linux" },
151
+ otherExtra: { osType: "windows" },
152
+ oversizedExtra: { hostId: OVERSIZED },
153
+ },
154
+ ];
155
+ /* eslint-enable @typescript-eslint/no-explicit-any */
156
+
157
+ describe.each(SERVICE_CASES)(
158
+ "$name.updateLastSeen",
159
+ ({ service, extra, otherExtra, oversizedExtra }: ServiceCase) => {
160
+ let writes: Array<WriteCall>;
161
+ let cache: Map<string, string>;
162
+ let deletedCacheKeys: Array<string>;
163
+
164
+ const RESOURCE_ID: ObjectID = ObjectID.generate();
165
+
166
+ /** Records every hook-free UPDATE instead of issuing it. */
167
+ function mockWritesSucceeding(): void {
168
+ jest
169
+ .spyOn(service, "updateColumnsByIdWithoutHooks")
170
+ .mockImplementation(async (input: { id: ObjectID; data: unknown }) => {
171
+ writes.push({
172
+ id: input.id,
173
+ data: { ...(input.data as Record<string, unknown>) },
174
+ });
175
+ });
176
+ }
177
+
178
+ /**
179
+ * Fails any write carrying more than the two liveness columns — the
180
+ * shape of a Postgres rejection caused by one bad optional column.
181
+ */
182
+ function mockEnrichedWriteFailing(): void {
183
+ jest
184
+ .spyOn(service, "updateColumnsByIdWithoutHooks")
185
+ .mockImplementation(async (input: { id: ObjectID; data: unknown }) => {
186
+ const data: Record<string, unknown> = {
187
+ ...(input.data as Record<string, unknown>),
188
+ };
189
+ writes.push({ id: input.id, data: data });
190
+ if (Object.keys(data).length > 2) {
191
+ throw new Error("value too long for type character varying(100)");
192
+ }
193
+ });
194
+ }
195
+
196
+ function lastWrite(): Record<string, unknown> {
197
+ return writes[writes.length - 1]!.data;
198
+ }
199
+
200
+ beforeEach(() => {
201
+ writes = [];
202
+ cache = new Map<string, string>();
203
+ deletedCacheKeys = [];
204
+
205
+ jest
206
+ .spyOn(GlobalCache, "getString")
207
+ .mockImplementation(async (namespace: string, key: string) => {
208
+ return cache.get(`${namespace}:${key}`) ?? null;
209
+ });
210
+ jest
211
+ .spyOn(GlobalCache, "setString")
212
+ .mockImplementation(
213
+ async (namespace: string, key: string, value: string) => {
214
+ cache.set(`${namespace}:${key}`, value);
215
+ },
216
+ );
217
+ jest
218
+ .spyOn(GlobalCache, "deleteKey")
219
+ .mockImplementation(async (namespace: string, key: string) => {
220
+ deletedCacheKeys.push(`${namespace}:${key}`);
221
+ cache.delete(`${namespace}:${key}`);
222
+ });
223
+ });
224
+
225
+ afterEach(() => {
226
+ jest.restoreAllMocks();
227
+ });
228
+
229
+ describe("liveness columns", () => {
230
+ beforeEach(() => {
231
+ mockWritesSucceeding();
232
+ });
233
+
234
+ test("always writes lastSeenAt and marks the collector connected", async () => {
235
+ await service.updateLastSeen(RESOURCE_ID);
236
+
237
+ expect(writes).toHaveLength(1);
238
+ expect(lastWrite()["otelCollectorStatus"]).toBe("connected");
239
+ expect(lastWrite()["lastSeenAt"]).toBeInstanceOf(Date);
240
+ });
241
+
242
+ test("writes to the resource it was given", async () => {
243
+ await service.updateLastSeen(RESOURCE_ID);
244
+
245
+ expect(writes[0]!.id.toString()).toBe(RESOURCE_ID.toString());
246
+ });
247
+
248
+ test("skips the write when the same metadata was written recently", async () => {
249
+ await service.updateLastSeen(RESOURCE_ID, extra as never);
250
+ await service.updateLastSeen(RESOURCE_ID, extra as never);
251
+
252
+ expect(writes).toHaveLength(1);
253
+ });
254
+
255
+ test("writes again as soon as any metadata changes", async () => {
256
+ await service.updateLastSeen(RESOURCE_ID, extra as never);
257
+ await service.updateLastSeen(RESOURCE_ID, otherExtra as never);
258
+
259
+ expect(writes).toHaveLength(2);
260
+ });
261
+
262
+ test("passes collector metadata through to the write path", async () => {
263
+ await service.updateLastSeen(RESOURCE_ID, extra as never);
264
+
265
+ for (const [column, value] of Object.entries(extra)) {
266
+ expect(lastWrite()[column]).toBe(value);
267
+ }
268
+ });
269
+ });
270
+
271
+ describe("fallback when the enriched write fails", () => {
272
+ beforeEach(() => {
273
+ mockEnrichedWriteFailing();
274
+ });
275
+
276
+ test("does not throw — ingest keeps going", async () => {
277
+ await expect(
278
+ service.updateLastSeen(RESOURCE_ID, oversizedExtra as never),
279
+ ).resolves.toBeUndefined();
280
+ });
281
+
282
+ test("retries with liveness only, so lastSeenAt still advances", async () => {
283
+ await service.updateLastSeen(RESOURCE_ID, oversizedExtra as never);
284
+
285
+ expect(writes).toHaveLength(2);
286
+ expect(Object.keys(lastWrite()).sort()).toEqual([
287
+ "lastSeenAt",
288
+ "otelCollectorStatus",
289
+ ]);
290
+ expect(lastWrite()["otelCollectorStatus"]).toBe("connected");
291
+ expect(lastWrite()["lastSeenAt"]).toBeInstanceOf(Date);
292
+ });
293
+
294
+ test("the retry targets the same resource", async () => {
295
+ await service.updateLastSeen(RESOURCE_ID, oversizedExtra as never);
296
+
297
+ expect(writes[1]!.id.toString()).toBe(RESOURCE_ID.toString());
298
+ });
299
+
300
+ test("busts the throttle cache so the next batch is not skipped", async () => {
301
+ await service.updateLastSeen(RESOURCE_ID, oversizedExtra as never);
302
+
303
+ expect(deletedCacheKeys).toHaveLength(1);
304
+ expect(deletedCacheKeys[0]).toContain(RESOURCE_ID.toString());
305
+ });
306
+
307
+ test("the next batch retries rather than short-circuiting on a cache hit", async () => {
308
+ await service.updateLastSeen(RESOURCE_ID, oversizedExtra as never);
309
+ writes = [];
310
+
311
+ await service.updateLastSeen(RESOURCE_ID, oversizedExtra as never);
312
+
313
+ // Enriched attempt + liveness fallback, not a silent no-op.
314
+ expect(writes).toHaveLength(2);
315
+ });
316
+
317
+ test("a failing cache delete does not stop the liveness retry", async () => {
318
+ jest
319
+ .spyOn(GlobalCache, "deleteKey")
320
+ .mockRejectedValue(new Error("redis down"));
321
+
322
+ await service.updateLastSeen(RESOURCE_ID, oversizedExtra as never);
323
+
324
+ expect(Object.keys(lastWrite()).sort()).toEqual([
325
+ "lastSeenAt",
326
+ "otelCollectorStatus",
327
+ ]);
328
+ });
329
+
330
+ test("surfaces a genuinely broken database instead of hiding it", async () => {
331
+ // Both attempts fail — the caller must find out.
332
+ jest
333
+ .spyOn(service, "updateColumnsByIdWithoutHooks")
334
+ .mockRejectedValue(new Error("connection terminated"));
335
+
336
+ await expect(
337
+ service.updateLastSeen(RESOURCE_ID, extra as never),
338
+ ).rejects.toThrow("connection terminated");
339
+ });
340
+ });
341
+
342
+ describe("cache outages fail open", () => {
343
+ beforeEach(() => {
344
+ mockWritesSucceeding();
345
+ });
346
+
347
+ test("a read failure still writes — never mark a live resource disconnected", async () => {
348
+ jest
349
+ .spyOn(GlobalCache, "getString")
350
+ .mockRejectedValue(new Error("redis down"));
351
+
352
+ await service.updateLastSeen(RESOURCE_ID, extra as never);
353
+
354
+ expect(writes).toHaveLength(1);
355
+ });
356
+
357
+ test("a write failure still writes", async () => {
358
+ jest
359
+ .spyOn(GlobalCache, "setString")
360
+ .mockRejectedValue(new Error("redis down"));
361
+
362
+ await service.updateLastSeen(RESOURCE_ID, extra as never);
363
+
364
+ expect(writes).toHaveLength(1);
365
+ });
366
+ });
367
+ },
368
+ );
369
+
370
+ describe("liveness fallback coverage", () => {
371
+ test("covers every service that writes liveness on the hook-free path", () => {
372
+ /*
373
+ * If a twelfth resource type grows an updateLastSeen, it belongs in
374
+ * SERVICE_CASES — that is the whole point of this suite. Bumping this
375
+ * number without adding the case defeats it.
376
+ */
377
+ expect(SERVICE_CASES).toHaveLength(11);
378
+ });
379
+
380
+ test("no service is listed twice", () => {
381
+ const names: Array<string> = SERVICE_CASES.map((c: ServiceCase) => {
382
+ return c.name;
383
+ });
384
+
385
+ expect(new Set(names).size).toBe(names.length);
386
+ });
387
+ });