@oneuptime/common 11.5.8 → 11.5.9

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 (195) hide show
  1. package/Models/DatabaseModels/AIAgentTaskPullRequest.ts +15 -6
  2. package/Models/DatabaseModels/AIRun.ts +37 -0
  3. package/Models/DatabaseModels/AIRunEvent.ts +34 -1
  4. package/Models/DatabaseModels/NetworkDeviceDiscoveryScan.ts +215 -0
  5. package/Models/DatabaseModels/Project.ts +25 -0
  6. package/Models/DatabaseModels/StatusPage.ts +42 -0
  7. package/Server/API/AIAgentTaskAPI.ts +38 -12
  8. package/Server/API/AIAgentTaskLogAPI.ts +49 -0
  9. package/Server/API/AIChatAPI.ts +16 -0
  10. package/Server/API/AIReadinessAPI.ts +84 -0
  11. package/Server/API/CodeFixRunAPI.ts +182 -0
  12. package/Server/API/TelemetryExceptionAPI.ts +6 -1
  13. package/Server/Infrastructure/Postgres/SchemaMigrations/1784124919694-AddAITaskNumberAndRunTranscript.ts +95 -0
  14. package/Server/Infrastructure/Postgres/SchemaMigrations/1784135099754-AllowNullAiAgentOnPullRequest.ts +52 -0
  15. package/Server/Infrastructure/Postgres/SchemaMigrations/1784137457184-AddEnableMcpServerToStatusPage.ts +30 -0
  16. package/Server/Infrastructure/Postgres/SchemaMigrations/1784200000000-AddSnmpV3ColumnsToNetworkDeviceDiscoveryScan.ts +49 -0
  17. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +8 -0
  18. package/Server/Services/AIRunEventService.ts +15 -1
  19. package/Server/Services/AIRunService.ts +38 -1
  20. package/Server/Services/LlmProviderService.ts +2 -0
  21. package/Server/Services/ProjectService.ts +42 -0
  22. package/Server/Services/StatusPageService.ts +87 -0
  23. package/Server/Services/TelemetryExceptionService.ts +19 -90
  24. package/Server/Types/Markdown.ts +9 -9
  25. package/Server/Types/MarkdownSlugify.ts +47 -0
  26. package/Server/Utils/AI/AIRunTranscript.ts +240 -0
  27. package/Server/Utils/AI/Chat/ChatAgentRunner.ts +9 -0
  28. package/Server/Utils/AI/Chat/ObservabilityChatPrompt.ts +119 -5
  29. package/Server/Utils/AI/CodeFix/CodeFixAgentCompletion.ts +75 -0
  30. package/Server/Utils/AI/CodeFix/CodeFixReadiness.ts +235 -0
  31. package/Server/Utils/AI/SRE/Insights/FixRouting.ts +2 -3
  32. package/Server/Utils/AI/Toolbox/CodeTools.ts +883 -0
  33. package/Server/Utils/AI/Toolbox/CodeWriteTools.ts +519 -0
  34. package/Server/Utils/AI/Toolbox/Index.ts +27 -0
  35. package/Server/Utils/AI/Toolbox/ScheduledMaintenanceTools.ts +279 -0
  36. package/Server/Utils/AnalyticsDatabase/ClickhouseCapacity.ts +19 -1
  37. package/Server/Utils/CodeRepository/GitHub/GitHub.ts +600 -0
  38. package/Server/Utils/CodeRepository/StackTraceRepoResolver.ts +7 -2
  39. package/Server/Utils/LLM/LLMService.ts +397 -18
  40. package/Server/Utils/Monitor/NetworkDeviceHydrationUtil.ts +4 -11
  41. package/Tests/Models/StatusPageEnableMcpServer.test.ts +125 -0
  42. package/Tests/Server/Services/StatusPageServiceMcp.test.ts +223 -0
  43. package/Tests/Server/Services/TelemetryExceptionAIFixReadiness.test.ts +26 -3
  44. package/Tests/Server/Services/TelemetryExceptionCodeFixRun.test.ts +12 -0
  45. package/Tests/Server/Types/Markdown.test.ts +106 -0
  46. package/Tests/Server/Utils/AI/AIRunTranscript.test.ts +182 -0
  47. package/Tests/Server/Utils/AI/CodeFixReadiness.test.ts +411 -0
  48. package/Tests/Server/Utils/AI/CodeTools.test.ts +596 -0
  49. package/Tests/Server/Utils/AI/CodeWriteTools.test.ts +475 -0
  50. package/Tests/Server/Utils/AI/LLMServiceModelCompatibility.test.ts +548 -0
  51. package/Tests/Server/Utils/AI/ObservabilityChatPrompt.test.ts +142 -0
  52. package/Tests/Server/Utils/AI/ScheduledMaintenanceTools.test.ts +176 -0
  53. package/Tests/Server/Utils/AnalyticsDatabase/ClickhouseCapacity.test.ts +63 -0
  54. package/Tests/Server/Utils/GitHubGetFileContent.test.ts +197 -0
  55. package/Tests/Server/Utils/Monitor/NetworkDeviceHydrationUtil.test.ts +502 -0
  56. package/Tests/Types/AI/AIAgentTaskStatus.test.ts +7 -6
  57. package/Tests/Types/AI/AIChatPageContext.test.ts +0 -0
  58. package/Tests/Types/AI/AIRunStatus.test.ts +42 -0
  59. package/Tests/Types/Log/LogSeverity.test.ts +78 -0
  60. package/Tests/UI/Components/BasicFormDropdownNormalization.test.tsx +167 -0
  61. package/Tests/UI/Components/Charts/AxisTickColor.test.tsx +66 -0
  62. package/Tests/UI/Utils/AIChatExport/ChatWidgetData.test.ts +319 -0
  63. package/Tests/UI/Utils/AIChatExport/ChatWidgetMarkdown.test.ts +188 -0
  64. package/Types/AI/AIAgentTaskStatus.ts +13 -0
  65. package/Types/AI/AIChatPageContext.ts +137 -0
  66. package/Types/AI/AIChatTypes.ts +44 -0
  67. package/Types/AI/AIFixReadiness.ts +46 -0
  68. package/Types/AI/AIRunStatus.ts +11 -0
  69. package/Types/Log/LogSeverity.ts +61 -0
  70. package/Types/Monitor/SnmpMonitor/SnmpVersion.ts +40 -0
  71. package/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.tsx +8 -2
  72. package/UI/Components/Charts/ChartLibrary/BarChart/BarChart.tsx +3 -1
  73. package/UI/Components/Charts/ChartLibrary/LineChart/LineChart.tsx +8 -2
  74. package/UI/Components/Forms/BasicForm.tsx +26 -8
  75. package/UI/Styles/Theme.css +1314 -0
  76. package/UI/Utils/AIChatExport/ChatWidgetData.ts +316 -0
  77. package/UI/Utils/AIChatExport/ChatWidgetMarkdown.ts +349 -0
  78. package/UI/Utils/AIChatExport/ConversationMarkdown.ts +177 -0
  79. package/UI/Utils/AIChatExport/MarkdownBlocks.ts +327 -0
  80. package/UI/Utils/AIChatExport/MarkdownSafety.ts +215 -0
  81. package/UI/Utils/DownloadFile.ts +54 -0
  82. package/build/dist/Models/DatabaseModels/AIAgentTaskPullRequest.js +15 -7
  83. package/build/dist/Models/DatabaseModels/AIAgentTaskPullRequest.js.map +1 -1
  84. package/build/dist/Models/DatabaseModels/AIRun.js +38 -0
  85. package/build/dist/Models/DatabaseModels/AIRun.js.map +1 -1
  86. package/build/dist/Models/DatabaseModels/AIRunEvent.js +31 -0
  87. package/build/dist/Models/DatabaseModels/AIRunEvent.js.map +1 -1
  88. package/build/dist/Models/DatabaseModels/NetworkDeviceDiscoveryScan.js +225 -0
  89. package/build/dist/Models/DatabaseModels/NetworkDeviceDiscoveryScan.js.map +1 -1
  90. package/build/dist/Models/DatabaseModels/Project.js +27 -0
  91. package/build/dist/Models/DatabaseModels/Project.js.map +1 -1
  92. package/build/dist/Models/DatabaseModels/StatusPage.js +43 -0
  93. package/build/dist/Models/DatabaseModels/StatusPage.js.map +1 -1
  94. package/build/dist/Server/API/AIAgentTaskAPI.js +37 -11
  95. package/build/dist/Server/API/AIAgentTaskAPI.js.map +1 -1
  96. package/build/dist/Server/API/AIAgentTaskLogAPI.js +32 -7
  97. package/build/dist/Server/API/AIAgentTaskLogAPI.js.map +1 -1
  98. package/build/dist/Server/API/AIChatAPI.js +9 -0
  99. package/build/dist/Server/API/AIChatAPI.js.map +1 -1
  100. package/build/dist/Server/API/AIReadinessAPI.js +57 -0
  101. package/build/dist/Server/API/AIReadinessAPI.js.map +1 -0
  102. package/build/dist/Server/API/CodeFixRunAPI.js +142 -0
  103. package/build/dist/Server/API/CodeFixRunAPI.js.map +1 -1
  104. package/build/dist/Server/API/TelemetryExceptionAPI.js +4 -0
  105. package/build/dist/Server/API/TelemetryExceptionAPI.js.map +1 -1
  106. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1784124919694-AddAITaskNumberAndRunTranscript.js +80 -0
  107. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1784124919694-AddAITaskNumberAndRunTranscript.js.map +1 -0
  108. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1784135099754-AllowNullAiAgentOnPullRequest.js +37 -0
  109. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1784135099754-AllowNullAiAgentOnPullRequest.js.map +1 -0
  110. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1784137457184-AddEnableMcpServerToStatusPage.js +23 -0
  111. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1784137457184-AddEnableMcpServerToStatusPage.js.map +1 -0
  112. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1784200000000-AddSnmpV3ColumnsToNetworkDeviceDiscoveryScan.js +22 -0
  113. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1784200000000-AddSnmpV3ColumnsToNetworkDeviceDiscoveryScan.js.map +1 -0
  114. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +8 -0
  115. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  116. package/build/dist/Server/Services/AIRunEventService.js +8 -0
  117. package/build/dist/Server/Services/AIRunEventService.js.map +1 -1
  118. package/build/dist/Server/Services/AIRunService.js +29 -0
  119. package/build/dist/Server/Services/AIRunService.js.map +1 -1
  120. package/build/dist/Server/Services/LlmProviderService.js +2 -0
  121. package/build/dist/Server/Services/LlmProviderService.js.map +1 -1
  122. package/build/dist/Server/Services/ProjectService.js +39 -0
  123. package/build/dist/Server/Services/ProjectService.js.map +1 -1
  124. package/build/dist/Server/Services/StatusPageService.js +81 -0
  125. package/build/dist/Server/Services/StatusPageService.js.map +1 -1
  126. package/build/dist/Server/Services/TelemetryExceptionService.js +14 -61
  127. package/build/dist/Server/Services/TelemetryExceptionService.js.map +1 -1
  128. package/build/dist/Server/Types/Markdown.js +8 -8
  129. package/build/dist/Server/Types/Markdown.js.map +1 -1
  130. package/build/dist/Server/Types/MarkdownSlugify.js +47 -0
  131. package/build/dist/Server/Types/MarkdownSlugify.js.map +1 -0
  132. package/build/dist/Server/Utils/AI/AIRunTranscript.js +180 -0
  133. package/build/dist/Server/Utils/AI/AIRunTranscript.js.map +1 -0
  134. package/build/dist/Server/Utils/AI/Chat/ChatAgentRunner.js +1 -0
  135. package/build/dist/Server/Utils/AI/Chat/ChatAgentRunner.js.map +1 -1
  136. package/build/dist/Server/Utils/AI/Chat/ObservabilityChatPrompt.js +103 -5
  137. package/build/dist/Server/Utils/AI/Chat/ObservabilityChatPrompt.js.map +1 -1
  138. package/build/dist/Server/Utils/AI/CodeFix/CodeFixAgentCompletion.js +55 -0
  139. package/build/dist/Server/Utils/AI/CodeFix/CodeFixAgentCompletion.js.map +1 -1
  140. package/build/dist/Server/Utils/AI/CodeFix/CodeFixReadiness.js +211 -0
  141. package/build/dist/Server/Utils/AI/CodeFix/CodeFixReadiness.js.map +1 -0
  142. package/build/dist/Server/Utils/AI/SRE/Insights/FixRouting.js.map +1 -1
  143. package/build/dist/Server/Utils/AI/Toolbox/CodeTools.js +654 -0
  144. package/build/dist/Server/Utils/AI/Toolbox/CodeTools.js.map +1 -0
  145. package/build/dist/Server/Utils/AI/Toolbox/CodeWriteTools.js +389 -0
  146. package/build/dist/Server/Utils/AI/Toolbox/CodeWriteTools.js.map +1 -0
  147. package/build/dist/Server/Utils/AI/Toolbox/Index.js +19 -0
  148. package/build/dist/Server/Utils/AI/Toolbox/Index.js.map +1 -1
  149. package/build/dist/Server/Utils/AI/Toolbox/ScheduledMaintenanceTools.js +239 -0
  150. package/build/dist/Server/Utils/AI/Toolbox/ScheduledMaintenanceTools.js.map +1 -0
  151. package/build/dist/Server/Utils/AnalyticsDatabase/ClickhouseCapacity.js +19 -1
  152. package/build/dist/Server/Utils/AnalyticsDatabase/ClickhouseCapacity.js.map +1 -1
  153. package/build/dist/Server/Utils/CodeRepository/GitHub/GitHub.js +439 -0
  154. package/build/dist/Server/Utils/CodeRepository/GitHub/GitHub.js.map +1 -1
  155. package/build/dist/Server/Utils/CodeRepository/StackTraceRepoResolver.js +1 -1
  156. package/build/dist/Server/Utils/CodeRepository/StackTraceRepoResolver.js.map +1 -1
  157. package/build/dist/Server/Utils/LLM/LLMService.js +275 -19
  158. package/build/dist/Server/Utils/LLM/LLMService.js.map +1 -1
  159. package/build/dist/Server/Utils/Monitor/NetworkDeviceHydrationUtil.js +2 -11
  160. package/build/dist/Server/Utils/Monitor/NetworkDeviceHydrationUtil.js.map +1 -1
  161. package/build/dist/Types/AI/AIAgentTaskStatus.js +12 -0
  162. package/build/dist/Types/AI/AIAgentTaskStatus.js.map +1 -1
  163. package/build/dist/Types/AI/AIChatPageContext.js +101 -0
  164. package/build/dist/Types/AI/AIChatPageContext.js.map +1 -0
  165. package/build/dist/Types/AI/AIChatTypes.js +2 -0
  166. package/build/dist/Types/AI/AIChatTypes.js.map +1 -1
  167. package/build/dist/Types/AI/AIFixReadiness.js +9 -0
  168. package/build/dist/Types/AI/AIFixReadiness.js.map +1 -0
  169. package/build/dist/Types/AI/AIRunStatus.js +11 -0
  170. package/build/dist/Types/AI/AIRunStatus.js.map +1 -1
  171. package/build/dist/Types/Log/LogSeverity.js +56 -0
  172. package/build/dist/Types/Log/LogSeverity.js.map +1 -1
  173. package/build/dist/Types/Monitor/SnmpMonitor/SnmpVersion.js +38 -0
  174. package/build/dist/Types/Monitor/SnmpMonitor/SnmpVersion.js.map +1 -1
  175. package/build/dist/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.js +9 -2
  176. package/build/dist/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.js.map +1 -1
  177. package/build/dist/UI/Components/Charts/ChartLibrary/BarChart/BarChart.js +3 -1
  178. package/build/dist/UI/Components/Charts/ChartLibrary/BarChart/BarChart.js.map +1 -1
  179. package/build/dist/UI/Components/Charts/ChartLibrary/LineChart/LineChart.js +9 -2
  180. package/build/dist/UI/Components/Charts/ChartLibrary/LineChart/LineChart.js.map +1 -1
  181. package/build/dist/UI/Components/Forms/BasicForm.js +24 -6
  182. package/build/dist/UI/Components/Forms/BasicForm.js.map +1 -1
  183. package/build/dist/UI/Utils/AIChatExport/ChatWidgetData.js +217 -0
  184. package/build/dist/UI/Utils/AIChatExport/ChatWidgetData.js.map +1 -0
  185. package/build/dist/UI/Utils/AIChatExport/ChatWidgetMarkdown.js +234 -0
  186. package/build/dist/UI/Utils/AIChatExport/ChatWidgetMarkdown.js.map +1 -0
  187. package/build/dist/UI/Utils/AIChatExport/ConversationMarkdown.js +111 -0
  188. package/build/dist/UI/Utils/AIChatExport/ConversationMarkdown.js.map +1 -0
  189. package/build/dist/UI/Utils/AIChatExport/MarkdownBlocks.js +233 -0
  190. package/build/dist/UI/Utils/AIChatExport/MarkdownBlocks.js.map +1 -0
  191. package/build/dist/UI/Utils/AIChatExport/MarkdownSafety.js +134 -0
  192. package/build/dist/UI/Utils/AIChatExport/MarkdownSafety.js.map +1 -0
  193. package/build/dist/UI/Utils/DownloadFile.js +41 -0
  194. package/build/dist/UI/Utils/DownloadFile.js.map +1 -0
  195. package/package.json +5 -1
@@ -0,0 +1,502 @@
1
+ import { afterEach, describe, expect, test } from "@jest/globals";
2
+ import NetworkDeviceHydrationUtil, {
3
+ HydratableMonitor,
4
+ } from "../../../../Server/Utils/Monitor/NetworkDeviceHydrationUtil";
5
+ import NetworkDeviceService from "../../../../Server/Services/NetworkDeviceService";
6
+ import NetworkDevice from "../../../../Models/DatabaseModels/NetworkDevice";
7
+ import MonitorStep from "../../../../Types/Monitor/MonitorStep";
8
+ import MonitorSteps from "../../../../Types/Monitor/MonitorSteps";
9
+ import MonitorType from "../../../../Types/Monitor/MonitorType";
10
+ import MonitorStepSnmpMonitor from "../../../../Types/Monitor/MonitorStepSnmpMonitor";
11
+ import SnmpVersion from "../../../../Types/Monitor/SnmpMonitor/SnmpVersion";
12
+ import SnmpV3Auth from "../../../../Types/Monitor/SnmpMonitor/SnmpV3Auth";
13
+ import SnmpSecurityLevel from "../../../../Types/Monitor/SnmpMonitor/SnmpSecurityLevel";
14
+ import SnmpAuthProtocol from "../../../../Types/Monitor/SnmpMonitor/SnmpAuthProtocol";
15
+ import SnmpPrivProtocol from "../../../../Types/Monitor/SnmpMonitor/SnmpPrivProtocol";
16
+ import ObjectID from "../../../../Types/ObjectID";
17
+
18
+ /*
19
+ * Network Device monitors carry only a reference to a NetworkDevice; the probe
20
+ * is stateless and understands nothing but concrete SNMP config. This util is
21
+ * the single place that turns the stored device into the `snmpMonitor` block a
22
+ * probe can execute, so it is where an SNMP v3 device either gets its
23
+ * credentials or silently loses them.
24
+ *
25
+ * Customer report: "Test Metrics" (Test Monitor) errors for a monitor on a v3
26
+ * device while v2 works. The probe throws when a v3 step reaches it without a
27
+ * v3 username (SnmpMonitor.createSnmpSession fails loudly rather than
28
+ * downgrading to v2c/"public"), so these tests pin exactly when this util does
29
+ * and does not produce snmpV3Auth — and that it hydrates the MonitorTest shape
30
+ * used by Test Monitor, not just saved Monitors.
31
+ */
32
+
33
+ const DEVICE_ID: string = "8f2c1f0e-0000-4000-8000-0000000000aa";
34
+
35
+ /*
36
+ * Columns are assigned loosely so a test can pin a column to undefined, which
37
+ * Partial<NetworkDevice> forbids under exactOptionalPropertyTypes.
38
+ */
39
+ type DeviceFields = Record<string, unknown>;
40
+
41
+ function buildDevice(fields: DeviceFields): NetworkDevice {
42
+ const device: NetworkDevice = new NetworkDevice();
43
+ device._id = DEVICE_ID;
44
+ device.hostname = "10.0.0.1";
45
+ Object.assign(device, fields);
46
+
47
+ return device;
48
+ }
49
+
50
+ function buildV3Device(fields?: DeviceFields): NetworkDevice {
51
+ return buildDevice({
52
+ snmpVersion: "V3",
53
+ snmpV3SecurityLevel: SnmpSecurityLevel.AuthPriv,
54
+ snmpV3Username: "monitoring",
55
+ snmpV3AuthProtocol: SnmpAuthProtocol.SHA,
56
+ snmpV3AuthKey: "auth-passphrase",
57
+ snmpV3PrivProtocol: SnmpPrivProtocol.AES,
58
+ snmpV3PrivKey: "priv-passphrase",
59
+ ...fields,
60
+ });
61
+ }
62
+
63
+ /*
64
+ * Structural stand-in for both Monitor and MonitorTest — hydrateNetworkDeviceMonitors
65
+ * accepts either, which is what the Test Monitor path depends on.
66
+ */
67
+ function buildMonitor(options?: {
68
+ monitorType?: MonitorType | undefined;
69
+ networkDeviceId?: string | undefined;
70
+ monitorInterfaces?: boolean | undefined;
71
+ }): HydratableMonitor {
72
+ const step: MonitorStep = new MonitorStep();
73
+ step.data = {
74
+ ...step.data,
75
+ networkDeviceMonitor: {
76
+ networkDeviceId:
77
+ options && "networkDeviceId" in options
78
+ ? options.networkDeviceId
79
+ : DEVICE_ID,
80
+ monitorInterfaces: options?.monitorInterfaces ?? true,
81
+ oids: [{ oid: "1.3.6.1.2.1.1.1.0", name: "sysDescr" }],
82
+ },
83
+ } as MonitorStep["data"];
84
+
85
+ const monitorSteps: MonitorSteps = new MonitorSteps();
86
+ monitorSteps.data = {
87
+ monitorStepsInstanceArray: [step],
88
+ defaultMonitorStatusId: undefined,
89
+ };
90
+
91
+ return {
92
+ id: ObjectID.generate(),
93
+ monitorType: options?.monitorType ?? MonitorType.NetworkDevice,
94
+ monitorSteps: monitorSteps,
95
+ };
96
+ }
97
+
98
+ function mockDevices(devices: Array<NetworkDevice>): void {
99
+ jest.spyOn(NetworkDeviceService, "findBy").mockResolvedValue(devices);
100
+ }
101
+
102
+ function hydratedStep(
103
+ monitor: HydratableMonitor,
104
+ ): MonitorStepSnmpMonitor | undefined {
105
+ return monitor.monitorSteps?.data?.monitorStepsInstanceArray[0]?.data
106
+ ?.snmpMonitor;
107
+ }
108
+
109
+ describe("NetworkDeviceHydrationUtil.hydrateNetworkDeviceMonitors", () => {
110
+ afterEach(() => {
111
+ jest.restoreAllMocks();
112
+ });
113
+
114
+ describe("SNMP v3 credential assembly", () => {
115
+ test("a v3 device hands the probe a complete authPriv SnmpV3Auth", async () => {
116
+ mockDevices([buildV3Device()]);
117
+ const monitor: HydratableMonitor = buildMonitor();
118
+
119
+ await NetworkDeviceHydrationUtil.hydrateNetworkDeviceMonitors([monitor]);
120
+
121
+ const snmp: MonitorStepSnmpMonitor | undefined = hydratedStep(monitor);
122
+
123
+ expect(snmp?.snmpVersion).toBe(SnmpVersion.V3);
124
+ expect(snmp?.snmpV3Auth).toEqual({
125
+ securityLevel: SnmpSecurityLevel.AuthPriv,
126
+ username: "monitoring",
127
+ authProtocol: SnmpAuthProtocol.SHA,
128
+ authKey: "auth-passphrase",
129
+ privProtocol: SnmpPrivProtocol.AES,
130
+ privKey: "priv-passphrase",
131
+ } as SnmpV3Auth);
132
+ });
133
+
134
+ /*
135
+ * The probe refuses to open a v3 session without a username. If this util
136
+ * ever hands back a v3 step with no auth, Test Monitor surfaces that
137
+ * refusal as the error the customer reported — so a fully configured
138
+ * device must never produce one.
139
+ */
140
+ test("a fully configured v3 device never yields a v3 step without credentials", async () => {
141
+ mockDevices([buildV3Device()]);
142
+ const monitor: HydratableMonitor = buildMonitor();
143
+
144
+ await NetworkDeviceHydrationUtil.hydrateNetworkDeviceMonitors([monitor]);
145
+
146
+ const snmp: MonitorStepSnmpMonitor | undefined = hydratedStep(monitor);
147
+
148
+ expect(snmp?.snmpVersion).toBe(SnmpVersion.V3);
149
+ expect(snmp?.snmpV3Auth?.username).toBeTruthy();
150
+ });
151
+
152
+ test("an authNoPriv device carries auth but no privacy material", async () => {
153
+ mockDevices([
154
+ buildV3Device({
155
+ snmpV3SecurityLevel: SnmpSecurityLevel.AuthNoPriv,
156
+ snmpV3PrivProtocol: undefined,
157
+ snmpV3PrivKey: undefined,
158
+ }),
159
+ ]);
160
+ const monitor: HydratableMonitor = buildMonitor();
161
+
162
+ await NetworkDeviceHydrationUtil.hydrateNetworkDeviceMonitors([monitor]);
163
+
164
+ const auth: SnmpV3Auth | undefined = hydratedStep(monitor)?.snmpV3Auth;
165
+
166
+ expect(auth?.securityLevel).toBe(SnmpSecurityLevel.AuthNoPriv);
167
+ expect(auth?.authKey).toBe("auth-passphrase");
168
+ expect(auth?.privProtocol).toBeUndefined();
169
+ expect(auth?.privKey).toBeUndefined();
170
+ });
171
+
172
+ test("a noAuthNoPriv device carries only the username", async () => {
173
+ mockDevices([
174
+ buildV3Device({
175
+ snmpV3SecurityLevel: SnmpSecurityLevel.NoAuthNoPriv,
176
+ snmpV3AuthProtocol: undefined,
177
+ snmpV3AuthKey: undefined,
178
+ snmpV3PrivProtocol: undefined,
179
+ snmpV3PrivKey: undefined,
180
+ }),
181
+ ]);
182
+ const monitor: HydratableMonitor = buildMonitor();
183
+
184
+ await NetworkDeviceHydrationUtil.hydrateNetworkDeviceMonitors([monitor]);
185
+
186
+ const auth: SnmpV3Auth | undefined = hydratedStep(monitor)?.snmpV3Auth;
187
+
188
+ expect(auth?.securityLevel).toBe(SnmpSecurityLevel.NoAuthNoPriv);
189
+ expect(auth?.username).toBe("monitoring");
190
+ expect(auth?.authKey).toBeUndefined();
191
+ });
192
+
193
+ test("a v3 device with no username yields no auth at all", async () => {
194
+ mockDevices([
195
+ buildDevice({ snmpVersion: "V3", snmpV3Username: undefined }),
196
+ ]);
197
+ const monitor: HydratableMonitor = buildMonitor();
198
+
199
+ await NetworkDeviceHydrationUtil.hydrateNetworkDeviceMonitors([monitor]);
200
+
201
+ expect(hydratedStep(monitor)?.snmpV3Auth).toBeUndefined();
202
+ });
203
+
204
+ test("a v3 device with an unset security level defaults to noAuthNoPriv", async () => {
205
+ mockDevices([
206
+ buildDevice({ snmpVersion: "V3", snmpV3Username: "monitoring" }),
207
+ ]);
208
+ const monitor: HydratableMonitor = buildMonitor();
209
+
210
+ await NetworkDeviceHydrationUtil.hydrateNetworkDeviceMonitors([monitor]);
211
+
212
+ expect(hydratedStep(monitor)?.snmpV3Auth?.securityLevel).toBe(
213
+ SnmpSecurityLevel.NoAuthNoPriv,
214
+ );
215
+ });
216
+ });
217
+
218
+ describe("legacy snmpV3Auth JSON column", () => {
219
+ test("a device predating the flattened columns still authenticates", async () => {
220
+ mockDevices([
221
+ buildDevice({
222
+ snmpVersion: "V3",
223
+ snmpV3Auth: {
224
+ securityLevel: SnmpSecurityLevel.AuthPriv,
225
+ username: "legacy-user",
226
+ authProtocol: SnmpAuthProtocol.MD5,
227
+ authKey: "legacy-auth",
228
+ privProtocol: SnmpPrivProtocol.DES,
229
+ privKey: "legacy-priv",
230
+ },
231
+ }),
232
+ ]);
233
+ const monitor: HydratableMonitor = buildMonitor();
234
+
235
+ await NetworkDeviceHydrationUtil.hydrateNetworkDeviceMonitors([monitor]);
236
+
237
+ expect(hydratedStep(monitor)?.snmpV3Auth?.username).toBe("legacy-user");
238
+ expect(hydratedStep(monitor)?.snmpV3Auth?.authProtocol).toBe(
239
+ SnmpAuthProtocol.MD5,
240
+ );
241
+ });
242
+
243
+ test("the flattened columns win over a stale legacy blob", async () => {
244
+ mockDevices([
245
+ buildV3Device({
246
+ snmpV3Auth: {
247
+ securityLevel: SnmpSecurityLevel.NoAuthNoPriv,
248
+ username: "stale-legacy-user",
249
+ },
250
+ }),
251
+ ]);
252
+ const monitor: HydratableMonitor = buildMonitor();
253
+
254
+ await NetworkDeviceHydrationUtil.hydrateNetworkDeviceMonitors([monitor]);
255
+
256
+ expect(hydratedStep(monitor)?.snmpV3Auth?.username).toBe("monitoring");
257
+ });
258
+
259
+ test("a legacy blob with no username is ignored", async () => {
260
+ mockDevices([
261
+ buildDevice({
262
+ snmpVersion: "V3",
263
+ snmpV3Auth: { securityLevel: SnmpSecurityLevel.AuthPriv },
264
+ }),
265
+ ]);
266
+ const monitor: HydratableMonitor = buildMonitor();
267
+
268
+ await NetworkDeviceHydrationUtil.hydrateNetworkDeviceMonitors([monitor]);
269
+
270
+ expect(hydratedStep(monitor)?.snmpV3Auth).toBeUndefined();
271
+ });
272
+ });
273
+
274
+ describe("SNMP version parsing from the stored column", () => {
275
+ /*
276
+ * The column is free text and both spellings exist in the wild: the
277
+ * dropdown writes enum keys, other writers and older defaults use the enum
278
+ * values. The probe branches on SnmpVersion, so both must map through.
279
+ */
280
+ test.each([
281
+ ["V3", SnmpVersion.V3],
282
+ ["3", SnmpVersion.V3],
283
+ ["v3", SnmpVersion.V3],
284
+ ["V1", SnmpVersion.V1],
285
+ ["1", SnmpVersion.V1],
286
+ ["v1", SnmpVersion.V1],
287
+ ["V2c", SnmpVersion.V2c],
288
+ ["2c", SnmpVersion.V2c],
289
+ ["v2c", SnmpVersion.V2c],
290
+ ])(
291
+ "a device storing snmpVersion=%s polls as %s",
292
+ async (stored: string, expected: SnmpVersion) => {
293
+ mockDevices([buildV3Device({ snmpVersion: stored })]);
294
+ const monitor: HydratableMonitor = buildMonitor();
295
+
296
+ await NetworkDeviceHydrationUtil.hydrateNetworkDeviceMonitors([
297
+ monitor,
298
+ ]);
299
+
300
+ expect(hydratedStep(monitor)?.snmpVersion).toBe(expected);
301
+ },
302
+ );
303
+
304
+ test("an unset or unrecognised version falls back to v2c", async () => {
305
+ for (const stored of [undefined, "", "nonsense"]) {
306
+ mockDevices([buildDevice({ snmpVersion: stored })]);
307
+ const monitor: HydratableMonitor = buildMonitor();
308
+
309
+ await NetworkDeviceHydrationUtil.hydrateNetworkDeviceMonitors([
310
+ monitor,
311
+ ]);
312
+
313
+ expect(hydratedStep(monitor)?.snmpVersion).toBe(SnmpVersion.V2c);
314
+ }
315
+ });
316
+
317
+ /*
318
+ * A v3 device whose version is stored in enum-value form must still reach
319
+ * the probe as v3 WITH its credentials — parsing the version and building
320
+ * the auth are independent, and getting only one right is what downgrades
321
+ * a device to a cleartext v2c poll.
322
+ */
323
+ test('a v3 device stored as "3" keeps both its version and its credentials', async () => {
324
+ mockDevices([buildV3Device({ snmpVersion: "3" })]);
325
+ const monitor: HydratableMonitor = buildMonitor();
326
+
327
+ await NetworkDeviceHydrationUtil.hydrateNetworkDeviceMonitors([monitor]);
328
+
329
+ expect(hydratedStep(monitor)?.snmpVersion).toBe(SnmpVersion.V3);
330
+ expect(hydratedStep(monitor)?.snmpV3Auth?.username).toBe("monitoring");
331
+ });
332
+ });
333
+
334
+ describe("v1/v2c community polling", () => {
335
+ test("a v2c device carries its community string and no v3 auth", async () => {
336
+ mockDevices([
337
+ buildDevice({ snmpVersion: "V2c", snmpCommunityString: "s3cret" }),
338
+ ]);
339
+ const monitor: HydratableMonitor = buildMonitor();
340
+
341
+ await NetworkDeviceHydrationUtil.hydrateNetworkDeviceMonitors([monitor]);
342
+
343
+ const snmp: MonitorStepSnmpMonitor | undefined = hydratedStep(monitor);
344
+
345
+ expect(snmp?.snmpVersion).toBe(SnmpVersion.V2c);
346
+ expect(snmp?.communityString).toBe("s3cret");
347
+ expect(snmp?.snmpV3Auth).toBeUndefined();
348
+ });
349
+
350
+ test("a v2c device with no community string leaves it unset for the probe to default", async () => {
351
+ mockDevices([buildDevice({ snmpVersion: "V2c" })]);
352
+ const monitor: HydratableMonitor = buildMonitor();
353
+
354
+ await NetworkDeviceHydrationUtil.hydrateNetworkDeviceMonitors([monitor]);
355
+
356
+ expect(hydratedStep(monitor)?.communityString).toBeUndefined();
357
+ });
358
+ });
359
+
360
+ describe("connection details and step options", () => {
361
+ test("hostname, port, oids and interface flag come from the device and step", async () => {
362
+ mockDevices([buildV3Device({ hostname: "192.168.5.9", snmpPort: 1610 })]);
363
+ const monitor: HydratableMonitor = buildMonitor();
364
+
365
+ await NetworkDeviceHydrationUtil.hydrateNetworkDeviceMonitors([monitor]);
366
+
367
+ const snmp: MonitorStepSnmpMonitor | undefined = hydratedStep(monitor);
368
+
369
+ expect(snmp?.hostname).toBe("192.168.5.9");
370
+ expect(snmp?.port).toBe(1610);
371
+ expect(snmp?.oids).toEqual([
372
+ { oid: "1.3.6.1.2.1.1.1.0", name: "sysDescr" },
373
+ ]);
374
+ expect(snmp?.monitorInterfaces).toBe(true);
375
+ });
376
+
377
+ test("a device with no port falls back to 161", async () => {
378
+ mockDevices([buildV3Device({ snmpPort: undefined })]);
379
+ const monitor: HydratableMonitor = buildMonitor();
380
+
381
+ await NetworkDeviceHydrationUtil.hydrateNetworkDeviceMonitors([monitor]);
382
+
383
+ expect(hydratedStep(monitor)?.port).toBe(161);
384
+ });
385
+
386
+ test("monitorInterfaces=false is respected rather than defaulted on", async () => {
387
+ mockDevices([buildV3Device()]);
388
+ const monitor: HydratableMonitor = buildMonitor({
389
+ monitorInterfaces: false,
390
+ });
391
+
392
+ await NetworkDeviceHydrationUtil.hydrateNetworkDeviceMonitors([monitor]);
393
+
394
+ expect(hydratedStep(monitor)?.monitorInterfaces).toBe(false);
395
+ });
396
+ });
397
+
398
+ describe("guards", () => {
399
+ test("a non-NetworkDevice monitor is left untouched", async () => {
400
+ const findBy: jest.SpyInstance = jest
401
+ .spyOn(NetworkDeviceService, "findBy")
402
+ .mockResolvedValue([buildV3Device()]);
403
+
404
+ const monitor: HydratableMonitor = buildMonitor({
405
+ monitorType: MonitorType.Website,
406
+ });
407
+
408
+ await NetworkDeviceHydrationUtil.hydrateNetworkDeviceMonitors([monitor]);
409
+
410
+ expect(hydratedStep(monitor)).toBeUndefined();
411
+ expect(findBy).not.toHaveBeenCalled();
412
+ });
413
+
414
+ test("a step referencing a deleted device is not hydrated", async () => {
415
+ mockDevices([]);
416
+ const monitor: HydratableMonitor = buildMonitor();
417
+
418
+ await NetworkDeviceHydrationUtil.hydrateNetworkDeviceMonitors([monitor]);
419
+
420
+ expect(hydratedStep(monitor)).toBeUndefined();
421
+ });
422
+
423
+ test("a step with no device reference is skipped without a lookup", async () => {
424
+ const findBy: jest.SpyInstance = jest
425
+ .spyOn(NetworkDeviceService, "findBy")
426
+ .mockResolvedValue([]);
427
+
428
+ const monitor: HydratableMonitor = buildMonitor({
429
+ networkDeviceId: undefined,
430
+ });
431
+
432
+ await NetworkDeviceHydrationUtil.hydrateNetworkDeviceMonitors([monitor]);
433
+
434
+ expect(hydratedStep(monitor)).toBeUndefined();
435
+ expect(findBy).not.toHaveBeenCalled();
436
+ });
437
+
438
+ test("a device with no hostname is not hydrated", async () => {
439
+ mockDevices([buildV3Device({ hostname: undefined })]);
440
+ const monitor: HydratableMonitor = buildMonitor();
441
+
442
+ await NetworkDeviceHydrationUtil.hydrateNetworkDeviceMonitors([monitor]);
443
+
444
+ expect(hydratedStep(monitor)).toBeUndefined();
445
+ });
446
+
447
+ test("an empty monitor list performs no lookup", async () => {
448
+ const findBy: jest.SpyInstance = jest
449
+ .spyOn(NetworkDeviceService, "findBy")
450
+ .mockResolvedValue([]);
451
+
452
+ await NetworkDeviceHydrationUtil.hydrateNetworkDeviceMonitors([]);
453
+
454
+ expect(findBy).not.toHaveBeenCalled();
455
+ });
456
+ });
457
+
458
+ describe("the Test Monitor path", () => {
459
+ /*
460
+ * Test Monitor posts a MonitorTest — a different model from Monitor, but
461
+ * structurally hydratable. It must receive the same v3 credentials the
462
+ * scheduled monitor does, or the test errors while live polling works.
463
+ */
464
+ test("a MonitorTest-shaped object gets the same v3 credentials as a Monitor", async () => {
465
+ mockDevices([buildV3Device()]);
466
+
467
+ const monitorTest: HydratableMonitor = buildMonitor();
468
+ const monitor: HydratableMonitor = buildMonitor();
469
+
470
+ await NetworkDeviceHydrationUtil.hydrateNetworkDeviceMonitors([
471
+ monitorTest,
472
+ ]);
473
+ mockDevices([buildV3Device()]);
474
+ await NetworkDeviceHydrationUtil.hydrateNetworkDeviceMonitors([monitor]);
475
+
476
+ expect(hydratedStep(monitorTest)).toEqual(hydratedStep(monitor));
477
+ expect(hydratedStep(monitorTest)?.snmpV3Auth?.username).toBe(
478
+ "monitoring",
479
+ );
480
+ });
481
+
482
+ test("many monitors sharing one device are all hydrated from a single lookup", async () => {
483
+ const findBy: jest.SpyInstance = jest
484
+ .spyOn(NetworkDeviceService, "findBy")
485
+ .mockResolvedValue([buildV3Device()]);
486
+
487
+ const monitors: Array<HydratableMonitor> = [
488
+ buildMonitor(),
489
+ buildMonitor(),
490
+ buildMonitor(),
491
+ ];
492
+
493
+ await NetworkDeviceHydrationUtil.hydrateNetworkDeviceMonitors(monitors);
494
+
495
+ expect(findBy).toHaveBeenCalledTimes(1);
496
+
497
+ for (const monitor of monitors) {
498
+ expect(hydratedStep(monitor)?.snmpV3Auth?.username).toBe("monitoring");
499
+ }
500
+ });
501
+ });
502
+ });
@@ -69,12 +69,13 @@ describe("AIAgentTaskStatusHelper", () => {
69
69
  });
70
70
 
71
71
  describe("isTerminalStatus", () => {
72
- test.each([AIAgentTaskStatus.Completed, AIAgentTaskStatus.Error])(
73
- "returns true for terminal status %s",
74
- (status: AIAgentTaskStatus) => {
75
- expect(AIAgentTaskStatusHelper.isTerminalStatus(status)).toBe(true);
76
- },
77
- );
72
+ test.each([
73
+ AIAgentTaskStatus.Completed,
74
+ AIAgentTaskStatus.NoFixFound,
75
+ AIAgentTaskStatus.Error,
76
+ ])("returns true for terminal status %s", (status: AIAgentTaskStatus) => {
77
+ expect(AIAgentTaskStatusHelper.isTerminalStatus(status)).toBe(true);
78
+ });
78
79
 
79
80
  test.each([AIAgentTaskStatus.Scheduled, AIAgentTaskStatus.InProgress])(
80
81
  "returns false for non-terminal status %s",
@@ -0,0 +1,42 @@
1
+ import AIRunStatus, { AIRunStatusHelper } from "../../../Types/AI/AIRunStatus";
2
+
3
+ describe("AIRunStatusHelper", () => {
4
+ describe("isTerminalStatus", () => {
5
+ test.each([
6
+ AIRunStatus.Completed,
7
+ AIRunStatus.NoFixFound,
8
+ AIRunStatus.Error,
9
+ AIRunStatus.Cancelled,
10
+ AIRunStatus.Stale,
11
+ ])("returns true for terminal status %s", (status: AIRunStatus) => {
12
+ expect(AIRunStatusHelper.isTerminalStatus(status)).toBe(true);
13
+ });
14
+
15
+ test.each([
16
+ AIRunStatus.Queued,
17
+ AIRunStatus.Running,
18
+ AIRunStatus.WaitingForApproval,
19
+ ])("returns false for non-terminal status %s", (status: AIRunStatus) => {
20
+ expect(AIRunStatusHelper.isTerminalStatus(status)).toBe(false);
21
+ });
22
+
23
+ /*
24
+ * A run that finished with no fix is DONE. Were it treated as non-terminal,
25
+ * the exception page would report a task still in flight forever: no retry
26
+ * offered, and the duplicate guard would refuse to start a new run.
27
+ */
28
+ test("NoFixFound is terminal — it is a result, not an in-flight run", () => {
29
+ expect(AIRunStatusHelper.isTerminalStatus(AIRunStatus.NoFixFound)).toBe(
30
+ true,
31
+ );
32
+ });
33
+
34
+ test("every status in the enum is classified", () => {
35
+ for (const status of Object.values(AIRunStatus)) {
36
+ expect(typeof AIRunStatusHelper.isTerminalStatus(status)).toBe(
37
+ "boolean",
38
+ );
39
+ }
40
+ });
41
+ });
42
+ });
@@ -0,0 +1,78 @@
1
+ import LogSeverity, {
2
+ LogSeverityNumber,
3
+ normalizeLogSeverity,
4
+ } from "../../../Types/Log/LogSeverity";
5
+
6
+ /*
7
+ * severityText on a log row is not what the client sent — ingest throws that
8
+ * away and re-derives it from severityNumber into one of these seven enum
9
+ * members. Filter comparisons are case-sensitive, so anything that writes or
10
+ * offers a severity-shaped string ("INFO", "warn") produces a value that
11
+ * matches nothing and reports no error. These tests pin the two things that
12
+ * prevent that: the enum is the only vocabulary, and the number always agrees
13
+ * with the text.
14
+ */
15
+ describe("LogSeverity", () => {
16
+ describe("LogSeverityNumber agrees with the OTLP ranges ingest buckets on", () => {
17
+ test.each([
18
+ [LogSeverity.Unspecified, 0],
19
+ [LogSeverity.Trace, 1],
20
+ [LogSeverity.Debug, 5],
21
+ [LogSeverity.Information, 9],
22
+ [LogSeverity.Warning, 13],
23
+ [LogSeverity.Error, 17],
24
+ [LogSeverity.Fatal, 21],
25
+ ])("%s -> %i", (severity: LogSeverity, expected: number) => {
26
+ expect(LogSeverityNumber[severity]).toBe(expected);
27
+ });
28
+
29
+ test("every enum member has a number", () => {
30
+ for (const severity of Object.values(LogSeverity)) {
31
+ expect(LogSeverityNumber[severity]).toBeDefined();
32
+ }
33
+ });
34
+ });
35
+
36
+ describe("normalizeLogSeverity rescues the values the old dropdowns emitted", () => {
37
+ /*
38
+ * These six are exactly what the filter builder and the severity remapper
39
+ * used to write. INFO and WARNING are the dangerous ones: they differ from
40
+ * the stored value by more than case, so no amount of case-insensitive
41
+ * comparison would have saved them.
42
+ */
43
+ test.each([
44
+ ["TRACE", LogSeverity.Trace],
45
+ ["DEBUG", LogSeverity.Debug],
46
+ ["INFO", LogSeverity.Information],
47
+ ["WARNING", LogSeverity.Warning],
48
+ ["ERROR", LogSeverity.Error],
49
+ ["FATAL", LogSeverity.Fatal],
50
+ ])("legacy %s -> %s", (legacy: string, expected: LogSeverity) => {
51
+ expect(normalizeLogSeverity(legacy)).toBe(expected);
52
+ });
53
+
54
+ test.each([
55
+ ["info", LogSeverity.Information],
56
+ ["Information", LogSeverity.Information],
57
+ ["warn", LogSeverity.Warning],
58
+ [" Error ", LogSeverity.Error],
59
+ ["err", LogSeverity.Error],
60
+ ["Unspecified", LogSeverity.Unspecified],
61
+ ])("%s -> %s", (input: string, expected: LogSeverity) => {
62
+ expect(normalizeLogSeverity(input)).toBe(expected);
63
+ });
64
+
65
+ test("every enum member normalizes to itself", () => {
66
+ for (const severity of Object.values(LogSeverity)) {
67
+ expect(normalizeLogSeverity(severity)).toBe(severity);
68
+ }
69
+ });
70
+
71
+ test.each([[""], [" "], ["nonsense"], ["INFORMATIONAL"], ["3"]])(
72
+ "%s -> null rather than a guess",
73
+ (input: string) => {
74
+ expect(normalizeLogSeverity(input)).toBeNull();
75
+ },
76
+ );
77
+ });
78
+ });