@oneuptime/common 11.5.12 → 11.6.0

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 (193) hide show
  1. package/Models/DatabaseModels/AIInsight.ts +30 -0
  2. package/Models/DatabaseModels/EnterpriseLicense.ts +21 -0
  3. package/Models/DatabaseModels/EnterpriseLicenseInstance.ts +25 -0
  4. package/Models/DatabaseModels/GlobalConfig.ts +76 -0
  5. package/Models/DatabaseModels/Project.ts +30 -0
  6. package/Models/DatabaseModels/TelemetryException.ts +128 -0
  7. package/Server/API/AIAgentDataAPI.ts +129 -5
  8. package/Server/API/AIInvestigationAPI.ts +87 -0
  9. package/Server/API/EnterpriseLicenseAPI.ts +57 -0
  10. package/Server/API/GlobalConfigAPI.ts +62 -1
  11. package/Server/EnvironmentConfig.ts +22 -0
  12. package/Server/Infrastructure/Postgres/SchemaMigrations/1784640000000-AddExceptionTriageAndPrivacyColumns.ts +56 -0
  13. package/Server/Infrastructure/Postgres/SchemaMigrations/1784659816363-AddInstanceVersionAndLatestReleaseColumns.ts +38 -0
  14. package/Server/Infrastructure/Postgres/SchemaMigrations/1784705487674-AddEnterpriseLicenseEvaluationColumns.ts +25 -0
  15. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +6 -0
  16. package/Server/Services/AIRunService.ts +8 -0
  17. package/Server/Services/AnalyticsDatabaseService.ts +48 -45
  18. package/Server/Services/DatabaseService.ts +66 -1
  19. package/Server/Services/TelemetryExceptionService.ts +320 -24
  20. package/Server/Services/WorkflowService.ts +100 -39
  21. package/Server/Types/Workflow/Components/Schedule.ts +57 -32
  22. package/Server/Utils/AI/SRE/Insights/Detectors/ExceptionSpikeDetector.ts +15 -4
  23. package/Server/Utils/AI/SRE/Insights/Detectors/NewExceptionDetector.ts +15 -4
  24. package/Server/Utils/AI/SRE/Insights/InsightScanner.ts +222 -24
  25. package/Server/Utils/AI/SRE/Insights/InsightTriageRunner.ts +225 -9
  26. package/Server/Utils/AI/SRE/SubjectCodeFixRun.ts +39 -0
  27. package/Server/Utils/AI/SRE/TelemetryImprovementTaskTrigger.ts +114 -0
  28. package/Server/Utils/AnalyticsDatabase/InsertDedupContext.ts +59 -0
  29. package/Server/Utils/Express.ts +13 -0
  30. package/Server/Utils/Telemetry/ExceptionSanitizer.ts +222 -0
  31. package/Server/Utils/Telemetry/TelemetryFanInWriter.ts +799 -0
  32. package/Server/Utils/Telemetry/TelemetryWriterClient.ts +263 -0
  33. package/Server/Utils/Telemetry/TelemetryWriterServer.ts +240 -0
  34. package/Server/Utils/Telemetry/TelemetryWriterShedMetrics.ts +93 -0
  35. package/Tests/Server/Services/DatabaseServiceSanitizeUpdateData.test.ts +232 -0
  36. package/Tests/Server/Services/TelemetryExceptionCodeFixRun.test.ts +205 -1
  37. package/Tests/Server/Services/UpdateOneByIdKeepsRowId.test.ts +107 -0
  38. package/Tests/Server/Services/WorkflowService.test.ts +308 -0
  39. package/Tests/Server/Utils/AI/Insights/InsightScanner.test.ts +150 -141
  40. package/Tests/Server/Utils/AI/Insights/InsightTriageRunner.test.ts +277 -1
  41. package/Tests/Server/Utils/Attribution.test.ts +203 -0
  42. package/Tests/Server/Utils/ExceptionSanitizer.test.ts +70 -0
  43. package/Tests/Server/Utils/InsightTriageClassification.test.ts +57 -0
  44. package/Tests/Server/Utils/Marketing/ConversionUploadProvider.test.ts +183 -0
  45. package/Tests/Server/Utils/Marketing/GoogleAds.test.ts +340 -0
  46. package/Tests/Server/Utils/Monitor/Criteria/ExceptionMonitorCriteria.test.ts +87 -0
  47. package/Tests/Server/Utils/Monitor/Criteria/LogMonitorCriteria.test.ts +165 -0
  48. package/Tests/Server/Utils/Monitor/Criteria/TraceMonitorCriteria.test.ts +86 -0
  49. package/Tests/Server/Utils/Telemetry/TelemetryFanInWriter.test.ts +1565 -0
  50. package/Tests/Server/Utils/Telemetry/TelemetryWriterClient.test.ts +313 -0
  51. package/Tests/Server/Utils/Telemetry/TelemetryWriterServer.test.ts +270 -0
  52. package/Tests/Server/Utils/Telemetry/TelemetryWriterShedMetrics.test.ts +127 -0
  53. package/Tests/Server/Utils/TelemetryImprovementTaskTrigger.test.ts +141 -0
  54. package/Tests/Types/AI/CodeFixTaskType.test.ts +3 -0
  55. package/Tests/Types/Monitor/MonitorCriteriaMetricVariables.test.ts +228 -0
  56. package/Tests/Types/Monitor/MonitorStepDefaultTelemetryConfig.test.ts +169 -0
  57. package/Tests/Types/Monitor/MonitorStepExceptionMonitor.test.ts +289 -0
  58. package/Tests/Types/Monitor/MonitorStepLogMonitor.test.ts +231 -0
  59. package/Tests/Types/Monitor/MonitorStepMetricViewConfigUtil.test.ts +245 -0
  60. package/Tests/Types/Monitor/MonitorStepNetworkDeviceMonitor.test.ts +155 -0
  61. package/Tests/Types/Monitor/MonitorStepTelemetrySerialization.test.ts +141 -0
  62. package/Tests/Types/Monitor/MonitorStepTraceMonitor.test.ts +137 -0
  63. package/Tests/Types/Workspace/NotificationRules/NotificationRuleCondition.test.ts +311 -0
  64. package/Tests/UI/Components/ModelTableSelectFromColumns.test.ts +259 -0
  65. package/Tests/UI/Utils/Breadcrumb/BreadcrumbTrailResolver.test.ts +461 -0
  66. package/Tests/UI/Utils/Breadcrumb/fixtures/RealBreadcrumbTrails.ts +2852 -0
  67. package/Tests/UI/Utils/Breadcrumb/fixtures/RealRoutePatterns.ts +758 -0
  68. package/Tests/UI/Utils/NotificationMethodUtil.test.ts +564 -0
  69. package/Tests/Utils/Array.test.ts +156 -0
  70. package/Tests/Utils/CronTab.test.ts +199 -0
  71. package/Tests/Utils/ModelImportExport.test.ts +101 -0
  72. package/Tests/Utils/Number.test.ts +179 -0
  73. package/Tests/Utils/VersionUtil.test.ts +348 -0
  74. package/Tests/jest.setup.ts +1 -0
  75. package/Types/AI/CodeFixTaskContext.ts +6 -0
  76. package/Types/AI/CodeFixTaskType.ts +38 -1
  77. package/Types/AI/ExceptionAIClassification.ts +27 -0
  78. package/Types/EnterpriseLicense/EnterpriseLicenseInstanceSummary.ts +6 -0
  79. package/Types/Log/LogScrubPatternType.ts +7 -0
  80. package/Types/Monitor/MonitorStep.ts +24 -4
  81. package/Types/Monitor/MonitorStepMetricViewConfigUtil.ts +105 -0
  82. package/Types/Trace/TraceScrubPatternType.ts +7 -0
  83. package/UI/Components/EditionLabel/EditionLabel.tsx +445 -12
  84. package/UI/Components/ModelTable/BaseModelTable.tsx +15 -46
  85. package/UI/Components/ModelTable/SelectFromColumns.ts +129 -0
  86. package/UI/Components/Workflow/ArgumentsForm.tsx +68 -30
  87. package/UI/Components/Workflow/CronScheduleField.tsx +411 -0
  88. package/UI/Utils/Breadcrumb/BreadcrumbTrailResolver.ts +176 -0
  89. package/UI/Utils/NotificationMethodUtil.ts +310 -0
  90. package/Utils/CronTab.ts +823 -0
  91. package/Utils/ModelImportExport.ts +21 -6
  92. package/Utils/VersionUtil.ts +260 -0
  93. package/build/dist/Models/DatabaseModels/AIInsight.js +31 -0
  94. package/build/dist/Models/DatabaseModels/AIInsight.js.map +1 -1
  95. package/build/dist/Models/DatabaseModels/EnterpriseLicense.js +22 -0
  96. package/build/dist/Models/DatabaseModels/EnterpriseLicense.js.map +1 -1
  97. package/build/dist/Models/DatabaseModels/EnterpriseLicenseInstance.js +26 -0
  98. package/build/dist/Models/DatabaseModels/EnterpriseLicenseInstance.js.map +1 -1
  99. package/build/dist/Models/DatabaseModels/GlobalConfig.js +81 -0
  100. package/build/dist/Models/DatabaseModels/GlobalConfig.js.map +1 -1
  101. package/build/dist/Models/DatabaseModels/Project.js +31 -0
  102. package/build/dist/Models/DatabaseModels/Project.js.map +1 -1
  103. package/build/dist/Models/DatabaseModels/TelemetryException.js +134 -0
  104. package/build/dist/Models/DatabaseModels/TelemetryException.js.map +1 -1
  105. package/build/dist/Server/API/AIAgentDataAPI.js +97 -8
  106. package/build/dist/Server/API/AIAgentDataAPI.js.map +1 -1
  107. package/build/dist/Server/API/AIInvestigationAPI.js +55 -0
  108. package/build/dist/Server/API/AIInvestigationAPI.js.map +1 -1
  109. package/build/dist/Server/API/EnterpriseLicenseAPI.js +47 -0
  110. package/build/dist/Server/API/EnterpriseLicenseAPI.js.map +1 -1
  111. package/build/dist/Server/API/GlobalConfigAPI.js +52 -1
  112. package/build/dist/Server/API/GlobalConfigAPI.js.map +1 -1
  113. package/build/dist/Server/EnvironmentConfig.js +17 -0
  114. package/build/dist/Server/EnvironmentConfig.js.map +1 -1
  115. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1784640000000-AddExceptionTriageAndPrivacyColumns.js +33 -0
  116. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1784640000000-AddExceptionTriageAndPrivacyColumns.js.map +1 -0
  117. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1784659816363-AddInstanceVersionAndLatestReleaseColumns.js +18 -0
  118. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1784659816363-AddInstanceVersionAndLatestReleaseColumns.js.map +1 -0
  119. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1784705487674-AddEnterpriseLicenseEvaluationColumns.js +14 -0
  120. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1784705487674-AddEnterpriseLicenseEvaluationColumns.js.map +1 -0
  121. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +6 -0
  122. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  123. package/build/dist/Server/Services/AIRunService.js +9 -2
  124. package/build/dist/Server/Services/AIRunService.js.map +1 -1
  125. package/build/dist/Server/Services/AnalyticsDatabaseService.js +44 -16
  126. package/build/dist/Server/Services/AnalyticsDatabaseService.js.map +1 -1
  127. package/build/dist/Server/Services/DatabaseService.js +53 -1
  128. package/build/dist/Server/Services/DatabaseService.js.map +1 -1
  129. package/build/dist/Server/Services/TelemetryExceptionService.js +263 -22
  130. package/build/dist/Server/Services/TelemetryExceptionService.js.map +1 -1
  131. package/build/dist/Server/Services/WorkflowService.js +80 -30
  132. package/build/dist/Server/Services/WorkflowService.js.map +1 -1
  133. package/build/dist/Server/Types/Workflow/Components/Schedule.js +44 -19
  134. package/build/dist/Server/Types/Workflow/Components/Schedule.js.map +1 -1
  135. package/build/dist/Server/Utils/AI/SRE/Insights/Detectors/ExceptionSpikeDetector.js +12 -4
  136. package/build/dist/Server/Utils/AI/SRE/Insights/Detectors/ExceptionSpikeDetector.js.map +1 -1
  137. package/build/dist/Server/Utils/AI/SRE/Insights/Detectors/NewExceptionDetector.js +12 -4
  138. package/build/dist/Server/Utils/AI/SRE/Insights/Detectors/NewExceptionDetector.js.map +1 -1
  139. package/build/dist/Server/Utils/AI/SRE/Insights/InsightScanner.js +193 -22
  140. package/build/dist/Server/Utils/AI/SRE/Insights/InsightScanner.js.map +1 -1
  141. package/build/dist/Server/Utils/AI/SRE/Insights/InsightTriageRunner.js +178 -9
  142. package/build/dist/Server/Utils/AI/SRE/Insights/InsightTriageRunner.js.map +1 -1
  143. package/build/dist/Server/Utils/AI/SRE/SubjectCodeFixRun.js +37 -0
  144. package/build/dist/Server/Utils/AI/SRE/SubjectCodeFixRun.js.map +1 -1
  145. package/build/dist/Server/Utils/AI/SRE/TelemetryImprovementTaskTrigger.js +98 -0
  146. package/build/dist/Server/Utils/AI/SRE/TelemetryImprovementTaskTrigger.js.map +1 -0
  147. package/build/dist/Server/Utils/AnalyticsDatabase/InsertDedupContext.js +24 -0
  148. package/build/dist/Server/Utils/AnalyticsDatabase/InsertDedupContext.js.map +1 -0
  149. package/build/dist/Server/Utils/Express.js +12 -0
  150. package/build/dist/Server/Utils/Express.js.map +1 -1
  151. package/build/dist/Server/Utils/Telemetry/ExceptionSanitizer.js +149 -0
  152. package/build/dist/Server/Utils/Telemetry/ExceptionSanitizer.js.map +1 -0
  153. package/build/dist/Server/Utils/Telemetry/TelemetryFanInWriter.js +496 -0
  154. package/build/dist/Server/Utils/Telemetry/TelemetryFanInWriter.js.map +1 -0
  155. package/build/dist/Server/Utils/Telemetry/TelemetryWriterClient.js +165 -0
  156. package/build/dist/Server/Utils/Telemetry/TelemetryWriterClient.js.map +1 -0
  157. package/build/dist/Server/Utils/Telemetry/TelemetryWriterServer.js +136 -0
  158. package/build/dist/Server/Utils/Telemetry/TelemetryWriterServer.js.map +1 -0
  159. package/build/dist/Server/Utils/Telemetry/TelemetryWriterShedMetrics.js +76 -0
  160. package/build/dist/Server/Utils/Telemetry/TelemetryWriterShedMetrics.js.map +1 -0
  161. package/build/dist/Types/AI/CodeFixTaskType.js +38 -1
  162. package/build/dist/Types/AI/CodeFixTaskType.js.map +1 -1
  163. package/build/dist/Types/AI/ExceptionAIClassification.js +28 -0
  164. package/build/dist/Types/AI/ExceptionAIClassification.js.map +1 -0
  165. package/build/dist/Types/Log/LogScrubPatternType.js +7 -0
  166. package/build/dist/Types/Log/LogScrubPatternType.js.map +1 -1
  167. package/build/dist/Types/Monitor/MonitorStep.js +20 -4
  168. package/build/dist/Types/Monitor/MonitorStep.js.map +1 -1
  169. package/build/dist/Types/Monitor/MonitorStepMetricViewConfigUtil.js +73 -0
  170. package/build/dist/Types/Monitor/MonitorStepMetricViewConfigUtil.js.map +1 -0
  171. package/build/dist/Types/Trace/TraceScrubPatternType.js +7 -0
  172. package/build/dist/Types/Trace/TraceScrubPatternType.js.map +1 -1
  173. package/build/dist/UI/Components/EditionLabel/EditionLabel.js +261 -13
  174. package/build/dist/UI/Components/EditionLabel/EditionLabel.js.map +1 -1
  175. package/build/dist/UI/Components/ModelTable/BaseModelTable.js +12 -37
  176. package/build/dist/UI/Components/ModelTable/BaseModelTable.js.map +1 -1
  177. package/build/dist/UI/Components/ModelTable/SelectFromColumns.js +82 -0
  178. package/build/dist/UI/Components/ModelTable/SelectFromColumns.js.map +1 -0
  179. package/build/dist/UI/Components/Workflow/ArgumentsForm.js +30 -6
  180. package/build/dist/UI/Components/Workflow/ArgumentsForm.js.map +1 -1
  181. package/build/dist/UI/Components/Workflow/CronScheduleField.js +209 -0
  182. package/build/dist/UI/Components/Workflow/CronScheduleField.js.map +1 -0
  183. package/build/dist/UI/Utils/Breadcrumb/BreadcrumbTrailResolver.js +129 -0
  184. package/build/dist/UI/Utils/Breadcrumb/BreadcrumbTrailResolver.js.map +1 -0
  185. package/build/dist/UI/Utils/NotificationMethodUtil.js +189 -0
  186. package/build/dist/UI/Utils/NotificationMethodUtil.js.map +1 -0
  187. package/build/dist/Utils/CronTab.js +609 -0
  188. package/build/dist/Utils/CronTab.js.map +1 -0
  189. package/build/dist/Utils/ModelImportExport.js +20 -6
  190. package/build/dist/Utils/ModelImportExport.js.map +1 -1
  191. package/build/dist/Utils/VersionUtil.js +193 -0
  192. package/build/dist/Utils/VersionUtil.js.map +1 -0
  193. package/package.json +1 -1
@@ -0,0 +1,232 @@
1
+ import NetworkDeviceDiscoveryScanService from "../../../Server/Services/NetworkDeviceDiscoveryScanService";
2
+ import ModelPermission from "../../../Server/Types/Database/Permissions/Index";
3
+ import UpdateBy from "../../../Server/Types/Database/UpdateBy";
4
+ import NetworkDeviceDiscoveryScan from "../../../Models/DatabaseModels/NetworkDeviceDiscoveryScan";
5
+ import DatabaseBaseModel from "../../../Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel";
6
+ import Probe from "../../../Models/DatabaseModels/Probe";
7
+ import ObjectID from "../../../Types/ObjectID";
8
+ import OneUptimeDate from "../../../Types/Date";
9
+ import { JSONObject } from "../../../Types/JSON";
10
+ import Select from "../../../Server/Types/Database/Select";
11
+ import { afterEach, describe, expect, jest, test } from "@jest/globals";
12
+
13
+ type ScanUpdateData = UpdateBy<NetworkDeviceDiscoveryScan>["data"];
14
+
15
+ /*
16
+ * Regression tests for the bug that left every SNMP discovery scan stuck in
17
+ * "Pending": update `data` passed as a full model instance carries the
18
+ * non-column own properties of DatabaseBaseModel (column initializers plus
19
+ * `isPermissionIf = {}`). _updateBy used to turn every data key into a select
20
+ * column for its internal find, and the unknown `isPermissionIf` column made
21
+ * that find throw `TableColumnMetadata not found for isPermissionIf column`,
22
+ * failing the whole update — so the probe-ingest list endpoint 400'd before
23
+ * it could claim a scan.
24
+ *
25
+ * sanitizeUpdateData is the fix: model instances are stripped down to their
26
+ * set table columns before the update pipeline runs.
27
+ */
28
+
29
+ describe("DatabaseService.sanitizeUpdateData — model instances become plain column data", () => {
30
+ test("a fresh model instance really does carry non-column own properties (the trap this guards against)", () => {
31
+ const scan: NetworkDeviceDiscoveryScan = new NetworkDeviceDiscoveryScan();
32
+
33
+ /*
34
+ * If this stops holding, the base model changed shape and these tests
35
+ * (and sanitizeUpdateData itself) should be revisited.
36
+ */
37
+ expect(Object.keys(scan)).toContain("isPermissionIf");
38
+ });
39
+
40
+ test("keeps only the columns that were actually set", () => {
41
+ const scan: NetworkDeviceDiscoveryScan = new NetworkDeviceDiscoveryScan();
42
+ scan.status = "In Progress";
43
+ scan.startedAt = OneUptimeDate.getCurrentDate();
44
+
45
+ const data: ScanUpdateData = NetworkDeviceDiscoveryScanService[
46
+ "sanitizeUpdateData"
47
+ ](scan as unknown as ScanUpdateData);
48
+
49
+ expect(Object.keys(data as JSONObject).sort()).toEqual([
50
+ "startedAt",
51
+ "status",
52
+ ]);
53
+ expect((data as JSONObject)["status"]).toBe("In Progress");
54
+ expect((data as JSONObject)["startedAt"]).toBeInstanceOf(Date);
55
+ });
56
+
57
+ test("the result is a plain object, not a model instance", () => {
58
+ const scan: NetworkDeviceDiscoveryScan = new NetworkDeviceDiscoveryScan();
59
+ scan.status = "Completed";
60
+
61
+ const data: ScanUpdateData = NetworkDeviceDiscoveryScanService[
62
+ "sanitizeUpdateData"
63
+ ](scan as unknown as ScanUpdateData);
64
+
65
+ expect(data).not.toBeInstanceOf(DatabaseBaseModel);
66
+ expect(Object.getPrototypeOf(data)).toBe(Object.prototype);
67
+ });
68
+
69
+ test("drops isPermissionIf and every other non-column own property", () => {
70
+ const scan: NetworkDeviceDiscoveryScan = new NetworkDeviceDiscoveryScan();
71
+ scan.status = "Completed";
72
+
73
+ const data: ScanUpdateData = NetworkDeviceDiscoveryScanService[
74
+ "sanitizeUpdateData"
75
+ ](scan as unknown as ScanUpdateData);
76
+
77
+ expect(Object.keys(data as JSONObject)).not.toContain("isPermissionIf");
78
+ });
79
+
80
+ test("drops columns left undefined so they cannot become writes", () => {
81
+ const scan: NetworkDeviceDiscoveryScan = new NetworkDeviceDiscoveryScan();
82
+ scan.status = "Failed";
83
+ // cidr, snmpVersion, statusMessage etc. stay undefined.
84
+
85
+ const data: ScanUpdateData = NetworkDeviceDiscoveryScanService[
86
+ "sanitizeUpdateData"
87
+ ](scan as unknown as ScanUpdateData);
88
+
89
+ expect(Object.keys(data as JSONObject)).toEqual(["status"]);
90
+ });
91
+
92
+ test("preserves explicit null so columns can still be cleared", () => {
93
+ const scan: NetworkDeviceDiscoveryScan = new NetworkDeviceDiscoveryScan();
94
+ scan.status = "Pending";
95
+ (scan as any).statusMessage = null;
96
+ (scan as any).nextScanAt = null;
97
+
98
+ const data: ScanUpdateData = NetworkDeviceDiscoveryScanService[
99
+ "sanitizeUpdateData"
100
+ ](scan as unknown as ScanUpdateData);
101
+
102
+ expect((data as JSONObject)["statusMessage"]).toBeNull();
103
+ expect((data as JSONObject)["nextScanAt"]).toBeNull();
104
+ });
105
+
106
+ test("drops _id, createdAt, updatedAt and version even when set (loaded-model updates must not redirect the write or forge timestamps)", () => {
107
+ const scan: NetworkDeviceDiscoveryScan = new NetworkDeviceDiscoveryScan(
108
+ ObjectID.generate(),
109
+ );
110
+ scan.status = "Completed";
111
+ (scan as any).createdAt = OneUptimeDate.getCurrentDate();
112
+ (scan as any).updatedAt = OneUptimeDate.getCurrentDate();
113
+ (scan as any).version = 7;
114
+
115
+ const data: ScanUpdateData = NetworkDeviceDiscoveryScanService[
116
+ "sanitizeUpdateData"
117
+ ](scan as unknown as ScanUpdateData);
118
+
119
+ expect(Object.keys(data as JSONObject)).toEqual(["status"]);
120
+ });
121
+
122
+ test("keeps entity relation columns that were set", () => {
123
+ const probeId: ObjectID = ObjectID.generate();
124
+ const scan: NetworkDeviceDiscoveryScan = new NetworkDeviceDiscoveryScan();
125
+ scan.status = "Pending";
126
+ scan.probe = new Probe(probeId);
127
+
128
+ const data: ScanUpdateData = NetworkDeviceDiscoveryScanService[
129
+ "sanitizeUpdateData"
130
+ ](scan as unknown as ScanUpdateData);
131
+
132
+ expect((data as JSONObject)["probe"]).toBeInstanceOf(Probe);
133
+ expect(((data as JSONObject)["probe"] as Probe).id?.toString()).toBe(
134
+ probeId.toString(),
135
+ );
136
+ });
137
+
138
+ test("plain objects pass through by reference, unknown keys and all (typos must keep failing loudly)", () => {
139
+ const plain: JSONObject = {
140
+ status: "Completed",
141
+ someTypoColumn: 1,
142
+ };
143
+
144
+ const data: ScanUpdateData = NetworkDeviceDiscoveryScanService[
145
+ "sanitizeUpdateData"
146
+ ](plain as unknown as ScanUpdateData);
147
+
148
+ expect(data).toBe(plain);
149
+ expect(Object.keys(data as JSONObject)).toContain("someTypoColumn");
150
+ });
151
+ });
152
+
153
+ describe("DatabaseService._updateBy — updateOneById with a model instance no longer poisons the internal find", () => {
154
+ afterEach(() => {
155
+ jest.restoreAllMocks();
156
+ });
157
+
158
+ test("select columns for the internal find contain only real columns from the data", async () => {
159
+ const scanId: ObjectID = ObjectID.generate();
160
+
161
+ const foundItem: NetworkDeviceDiscoveryScan =
162
+ new NetworkDeviceDiscoveryScan();
163
+ foundItem._id = scanId.toString();
164
+
165
+ // eslint-disable-next-line @typescript-eslint/typedef
166
+ const findBySpy = jest
167
+ .spyOn(NetworkDeviceDiscoveryScanService as any, "_findBy")
168
+ .mockResolvedValue([foundItem] as never);
169
+
170
+ // eslint-disable-next-line @typescript-eslint/typedef
171
+ const saveMock = jest.fn((item: unknown) => {
172
+ return Promise.resolve(item);
173
+ });
174
+ jest
175
+ .spyOn(NetworkDeviceDiscoveryScanService, "getRepository")
176
+ .mockReturnValue({ save: saveMock } as never);
177
+
178
+ // Permission layer is not under test here and needs no DB.
179
+ jest
180
+ .spyOn(ModelPermission, "checkUpdatePermissionByModel")
181
+ .mockResolvedValue(undefined as never);
182
+ jest
183
+ .spyOn(ModelPermission, "checkUpdateQueryPermissions")
184
+ .mockImplementation(((
185
+ _modelType: unknown,
186
+ query: unknown,
187
+ ): Promise<unknown> => {
188
+ return Promise.resolve(query);
189
+ }) as never);
190
+
191
+ const inProgress: NetworkDeviceDiscoveryScan =
192
+ new NetworkDeviceDiscoveryScan();
193
+ inProgress.status = "In Progress";
194
+ inProgress.startedAt = OneUptimeDate.getCurrentDate();
195
+
196
+ /*
197
+ * Before the fix this call rejected with "TableColumnMetadata not found
198
+ * for isPermissionIf column" — reproducing exactly why discovery scans
199
+ * never left Pending.
200
+ */
201
+ await expect(
202
+ NetworkDeviceDiscoveryScanService.updateOneById({
203
+ id: scanId,
204
+ data: inProgress as unknown as UpdateBy<NetworkDeviceDiscoveryScan>["data"],
205
+ props: {
206
+ isRoot: true,
207
+ },
208
+ }),
209
+ ).resolves.toBeUndefined();
210
+
211
+ expect(findBySpy).toHaveBeenCalledTimes(1);
212
+ const findArgs: { select: Select<NetworkDeviceDiscoveryScan> } = (
213
+ findBySpy.mock.calls[0] as Array<any>
214
+ )[0];
215
+ const selectedColumns: Array<string> = Object.keys(findArgs.select);
216
+
217
+ // The data columns plus _id and the tenant column — nothing else.
218
+ expect(selectedColumns.sort()).toEqual(
219
+ ["_id", "projectId", "startedAt", "status"].sort(),
220
+ );
221
+ expect(selectedColumns).not.toContain("isPermissionIf");
222
+
223
+ // The persisted payload is the plain data plus the located row's _id.
224
+ expect(saveMock).toHaveBeenCalledTimes(1);
225
+ const savedItem: JSONObject = saveMock.mock.calls[0]![0] as JSONObject;
226
+ expect(Object.keys(savedItem).sort()).toEqual(
227
+ ["_id", "startedAt", "status"].sort(),
228
+ );
229
+ expect(savedItem["status"]).toBe("In Progress");
230
+ expect(savedItem["_id"]).toBe(scanId.toString());
231
+ });
232
+ });
@@ -6,6 +6,7 @@ import ServiceService from "../../../Server/Services/ServiceService";
6
6
  import CodeRepositoryService from "../../../Server/Services/CodeRepositoryService";
7
7
  import AIAgentService from "../../../Server/Services/AIAgentService";
8
8
  import AIRunService from "../../../Server/Services/AIRunService";
9
+ import AIAgentTaskPullRequestService from "../../../Server/Services/AIAgentTaskPullRequestService";
9
10
  import { RepoResolution } from "../../../Server/Utils/CodeRepository/StackTraceRepoResolver";
10
11
  import FindOneBy from "../../../Server/Types/Database/FindOneBy";
11
12
  import TelemetryException from "../../../Models/DatabaseModels/TelemetryException";
@@ -37,12 +38,19 @@ const projectId: ObjectID = ObjectID.generate();
37
38
  const exceptionId: ObjectID = ObjectID.generate();
38
39
  const serviceId: ObjectID = ObjectID.generate();
39
40
 
40
- function fakeException(): TelemetryException {
41
+ function fakeException(
42
+ overrides: Partial<TelemetryException> = {},
43
+ ): TelemetryException {
41
44
  return {
42
45
  id: exceptionId,
43
46
  projectId: projectId,
44
47
  primaryEntityId: serviceId,
48
+ message: "Failed to charge card for user 123",
49
+ exceptionType: "PaymentError",
45
50
  stackTrace: "at charge (/app/src/billing/charge.ts:12:5)",
51
+ isResolved: false,
52
+ isArchived: false,
53
+ ...overrides,
46
54
  } as unknown as TelemetryException;
47
55
  }
48
56
 
@@ -91,6 +99,14 @@ function mockReadinessOk(): void {
91
99
  limitInTokens: null,
92
100
  usedTokensToday: 0,
93
101
  });
102
+
103
+ /*
104
+ * Cross-group duplicate guard collaborators: no non-terminal runs and no
105
+ * open AI pull requests unless a test overrides these. findBy on the
106
+ * exception service itself is only reached when candidates exist.
107
+ */
108
+ jest.spyOn(AIRunService, "findBy").mockResolvedValue([]);
109
+ jest.spyOn(AIAgentTaskPullRequestService, "findBy").mockResolvedValue([]);
94
110
  }
95
111
 
96
112
  describe("TelemetryExceptionService.createCodeFixRunForException", () => {
@@ -283,6 +299,194 @@ describe("TelemetryExceptionService.createCodeFixRunForException", () => {
283
299
  }),
284
300
  );
285
301
  });
302
+
303
+ test("rejects resolved and archived exceptions server-side", async () => {
304
+ mockReadinessOk();
305
+ const create: jest.SpyInstance = jest.spyOn(AIRunService, "create");
306
+
307
+ jest
308
+ .spyOn(TelemetryExceptionService, "findOneById")
309
+ .mockResolvedValue(fakeException({ isResolved: true }));
310
+
311
+ await expect(
312
+ TelemetryExceptionService.createCodeFixRunForException({
313
+ telemetryExceptionId: exceptionId,
314
+ props: { isRoot: true },
315
+ }),
316
+ ).rejects.toThrow(/marked as resolved/);
317
+
318
+ jest
319
+ .spyOn(TelemetryExceptionService, "findOneById")
320
+ .mockResolvedValue(fakeException({ isArchived: true }));
321
+
322
+ await expect(
323
+ TelemetryExceptionService.createCodeFixRunForException({
324
+ telemetryExceptionId: exceptionId,
325
+ props: { isRoot: true },
326
+ }),
327
+ ).rejects.toThrow(/archived/);
328
+
329
+ expect(create).not.toHaveBeenCalled();
330
+ });
331
+
332
+ test("a declined AI fix (aiFixDeclinedAt) blocks the automatic lane but a user click clears the stamp and proceeds", async () => {
333
+ mockReadinessOk();
334
+
335
+ jest
336
+ .spyOn(TelemetryExceptionService, "findOneById")
337
+ .mockResolvedValue(fakeException({ aiFixDeclinedAt: new Date() }));
338
+ jest.spyOn(AIRunService, "findOneBy").mockResolvedValue(null);
339
+ const create: jest.SpyInstance = jest
340
+ .spyOn(AIRunService, "create")
341
+ .mockResolvedValue({ id: ObjectID.generate() } as unknown as AIRun);
342
+ const clearStamp: jest.SpyInstance = jest
343
+ .spyOn(TelemetryExceptionService, "updateOneById")
344
+ .mockResolvedValue(undefined as never);
345
+
346
+ // System-triggered (no userId): blocked.
347
+ await expect(
348
+ TelemetryExceptionService.createCodeFixRunForException({
349
+ telemetryExceptionId: exceptionId,
350
+ props: { isRoot: true },
351
+ }),
352
+ ).rejects.toThrow(/closed without merging/);
353
+ expect(create).not.toHaveBeenCalled();
354
+
355
+ // A human click: clears the stamp and creates the run.
356
+ await expect(
357
+ TelemetryExceptionService.createCodeFixRunForException({
358
+ telemetryExceptionId: exceptionId,
359
+ props: { isRoot: true, userId: ObjectID.generate() },
360
+ }),
361
+ ).resolves.toBeDefined();
362
+
363
+ expect(clearStamp).toHaveBeenCalledWith(
364
+ expect.objectContaining({
365
+ id: exceptionId,
366
+ data: expect.objectContaining({ aiFixDeclinedAt: null }),
367
+ }),
368
+ );
369
+ expect(create).toHaveBeenCalled();
370
+ });
371
+
372
+ test("the decline stamp is scoped to FixException: other recipes are neither blocked by it nor clear it", async () => {
373
+ mockReadinessOk();
374
+
375
+ jest
376
+ .spyOn(TelemetryExceptionService, "findOneById")
377
+ .mockResolvedValue(fakeException({ aiFixDeclinedAt: new Date() }));
378
+ jest.spyOn(AIRunService, "findOneBy").mockResolvedValue(null);
379
+ const create: jest.SpyInstance = jest
380
+ .spyOn(AIRunService, "create")
381
+ .mockResolvedValue({ id: ObjectID.generate() } as unknown as AIRun);
382
+ const exceptionUpdates: jest.SpyInstance = jest
383
+ .spyOn(TelemetryExceptionService, "updateOneById")
384
+ .mockResolvedValue(undefined as never);
385
+
386
+ /*
387
+ * A user asking for a regression TEST on a group whose FIX a human
388
+ * declined: the test run proceeds, and — critically — the fix-decline
389
+ * suppression stays in force (the user never overrode the fix).
390
+ */
391
+ await expect(
392
+ TelemetryExceptionService.createCodeFixRunForException({
393
+ telemetryExceptionId: exceptionId,
394
+ props: { isRoot: true, userId: ObjectID.generate() },
395
+ taskType: CodeFixTaskType.WriteRegressionTest,
396
+ }),
397
+ ).resolves.toBeDefined();
398
+
399
+ expect(create).toHaveBeenCalled();
400
+ expect(exceptionUpdates).not.toHaveBeenCalledWith(
401
+ expect.objectContaining({
402
+ data: expect.objectContaining({ aiFixDeclinedAt: null }),
403
+ }),
404
+ );
405
+ });
406
+
407
+ test("cross-group duplicate guard: a similar exception (same type + normalized message) with an active run blocks creation", async () => {
408
+ mockReadinessOk();
409
+
410
+ /*
411
+ * The observed flood shape: one throw site interpolating a different
412
+ * UUID per request splinters into one fingerprint (and one exception
413
+ * group) per value. Normalization collapses both messages to
414
+ * `...uuid: "<UUID>"`, which is what the guard compares.
415
+ */
416
+ jest.spyOn(TelemetryExceptionService, "findOneById").mockResolvedValue(
417
+ fakeException({
418
+ message:
419
+ 'invalid input syntax for type uuid: "550e8400-e29b-41d4-a716-446655440000"',
420
+ exceptionType: "QueryFailedError",
421
+ }),
422
+ );
423
+
424
+ // Per-exception guard finds nothing for THIS exception...
425
+ jest.spyOn(AIRunService, "findOneBy").mockResolvedValue(null);
426
+
427
+ /*
428
+ * ...but another exception group — same root cause, different
429
+ * interpolated value — has a non-terminal FixException run.
430
+ */
431
+ const similarExceptionId: ObjectID = ObjectID.generate();
432
+ jest.spyOn(AIRunService, "findBy").mockResolvedValue([
433
+ {
434
+ triggeredByTelemetryExceptionId: similarExceptionId,
435
+ } as unknown as AIRun,
436
+ ]);
437
+ jest.spyOn(TelemetryExceptionService, "findBy").mockResolvedValue([
438
+ {
439
+ _id: similarExceptionId.toString(),
440
+ message:
441
+ 'invalid input syntax for type uuid: "123e4567-e89b-12d3-a456-426614174000"',
442
+ exceptionType: "QueryFailedError",
443
+ } as unknown as TelemetryException,
444
+ ]);
445
+
446
+ const create: jest.SpyInstance = jest.spyOn(AIRunService, "create");
447
+
448
+ await expect(
449
+ TelemetryExceptionService.createCodeFixRunForException({
450
+ telemetryExceptionId: exceptionId,
451
+ props: { isRoot: true },
452
+ }),
453
+ ).rejects.toThrow(/similar exception/);
454
+
455
+ expect(create).not.toHaveBeenCalled();
456
+ });
457
+
458
+ test("cross-group duplicate guard: a DIFFERENT normalized message does not block", async () => {
459
+ mockReadinessOk();
460
+
461
+ jest.spyOn(AIRunService, "findOneBy").mockResolvedValue(null);
462
+
463
+ const otherExceptionId: ObjectID = ObjectID.generate();
464
+ jest.spyOn(AIRunService, "findBy").mockResolvedValue([
465
+ {
466
+ triggeredByTelemetryExceptionId: otherExceptionId,
467
+ } as unknown as AIRun,
468
+ ]);
469
+ jest.spyOn(TelemetryExceptionService, "findBy").mockResolvedValue([
470
+ {
471
+ _id: otherExceptionId.toString(),
472
+ message: "Connection refused to redis",
473
+ exceptionType: "ConnectionError",
474
+ } as unknown as TelemetryException,
475
+ ]);
476
+
477
+ const create: jest.SpyInstance = jest
478
+ .spyOn(AIRunService, "create")
479
+ .mockResolvedValue({ id: ObjectID.generate() } as unknown as AIRun);
480
+
481
+ await expect(
482
+ TelemetryExceptionService.createCodeFixRunForException({
483
+ telemetryExceptionId: exceptionId,
484
+ props: { isRoot: true },
485
+ }),
486
+ ).resolves.toBeDefined();
487
+
488
+ expect(create).toHaveBeenCalled();
489
+ });
286
490
  });
287
491
 
288
492
  /*
@@ -0,0 +1,107 @@
1
+ import NetworkDeviceDiscoveryScan from "../../../Models/DatabaseModels/NetworkDeviceDiscoveryScan";
2
+ import NetworkDeviceDiscoveryScanService from "../../../Server/Services/NetworkDeviceDiscoveryScanService";
3
+ import ObjectID from "../../../Types/ObjectID";
4
+ import { afterEach, describe, expect, test } from "@jest/globals";
5
+
6
+ /*
7
+ * Regression tests for the second layer of the discovery-scan "stuck
8
+ * Pending" bug (the first layer — model instances poisoning the internal
9
+ * find's select columns — is covered in
10
+ * DatabaseServiceSanitizeUpdateData.test.ts).
11
+ *
12
+ * DatabaseService._updateBy builds the row it saves as a merge of the
13
+ * located row's _id and the caller's update data. With the old merge order
14
+ * `{ _id: item._id, ...data }`, update data carrying an own `_id: undefined`
15
+ * property (a fresh model instance before sanitizeUpdateData existed, or a
16
+ * plain object that spells it out) clobbered the located row's id — TypeORM
17
+ * save() saw no primary key, INSERTed a new row instead of updating, and
18
+ * died on the first NOT NULL column.
19
+ *
20
+ * These tests pin the fixed merge order: the located row's _id must win no
21
+ * matter what shape the update data takes. _findBy is mocked out, so this
22
+ * exercises the save-payload merge in isolation.
23
+ */
24
+
25
+ type SavedRow = { _id?: string; status?: string; startedAt?: Date };
26
+
27
+ function mockPersistence(existingRowId: string): jest.Mock {
28
+ const existing: NetworkDeviceDiscoveryScan = new NetworkDeviceDiscoveryScan();
29
+ existing._id = existingRowId;
30
+
31
+ jest
32
+ .spyOn(NetworkDeviceDiscoveryScanService as any, "_findBy")
33
+ .mockResolvedValue([existing] as never);
34
+
35
+ const save: jest.Mock = jest
36
+ .fn()
37
+ .mockImplementation(async (row: unknown): Promise<unknown> => {
38
+ return row;
39
+ });
40
+
41
+ jest
42
+ .spyOn(NetworkDeviceDiscoveryScanService, "getRepository")
43
+ .mockReturnValue({ save } as any);
44
+
45
+ return save;
46
+ }
47
+
48
+ describe("DatabaseService.updateOneById — saved row keeps the located row's _id", () => {
49
+ afterEach(() => {
50
+ jest.restoreAllMocks();
51
+ });
52
+
53
+ test("when data is a fresh model instance (own undefined _id property)", async () => {
54
+ const rowId: string = ObjectID.generate().toString();
55
+ const save: jest.Mock = mockPersistence(rowId);
56
+
57
+ const data: NetworkDeviceDiscoveryScan = new NetworkDeviceDiscoveryScan();
58
+ data.status = "In Progress";
59
+ data.startedAt = new Date();
60
+
61
+ // The bug's precondition: the instance really does carry _id: undefined.
62
+ expect(Object.keys(data)).toContain("_id");
63
+
64
+ await NetworkDeviceDiscoveryScanService.updateOneById({
65
+ id: new ObjectID(rowId),
66
+ data: data as never,
67
+ props: { isRoot: true },
68
+ });
69
+
70
+ expect(save).toHaveBeenCalledTimes(1);
71
+ const saved: SavedRow = save.mock.calls[0]![0] as SavedRow;
72
+ expect(saved._id).toBe(rowId);
73
+ expect(saved.status).toBe("In Progress");
74
+ });
75
+
76
+ test("when data is a plain partial object", async () => {
77
+ const rowId: string = ObjectID.generate().toString();
78
+ const save: jest.Mock = mockPersistence(rowId);
79
+
80
+ await NetworkDeviceDiscoveryScanService.updateOneById({
81
+ id: new ObjectID(rowId),
82
+ data: { status: "In Progress" } as never,
83
+ props: { isRoot: true },
84
+ });
85
+
86
+ expect(save).toHaveBeenCalledTimes(1);
87
+ const saved: SavedRow = save.mock.calls[0]![0] as SavedRow;
88
+ expect(saved._id).toBe(rowId);
89
+ expect(saved.status).toBe("In Progress");
90
+ });
91
+
92
+ test("when data is a plain object that spells out _id: undefined", async () => {
93
+ const rowId: string = ObjectID.generate().toString();
94
+ const save: jest.Mock = mockPersistence(rowId);
95
+
96
+ await NetworkDeviceDiscoveryScanService.updateOneById({
97
+ id: new ObjectID(rowId),
98
+ data: { _id: undefined, status: "In Progress" } as never,
99
+ props: { isRoot: true },
100
+ });
101
+
102
+ expect(save).toHaveBeenCalledTimes(1);
103
+ const saved: SavedRow = save.mock.calls[0]![0] as SavedRow;
104
+ expect(saved._id).toBe(rowId);
105
+ expect(saved.status).toBe("In Progress");
106
+ });
107
+ });