@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,223 @@
1
+ /**
2
+ * StatusPageService MCP gate tests.
3
+ *
4
+ * resolveStatusPageIdOrNull and isMcpServerEnabled back the unauthenticated
5
+ * public status page MCP tools, so their contract is security-relevant:
6
+ * - a status page that does not exist and one that has MCP turned off must be
7
+ * indistinguishable to the caller (otherwise the tools become a status page
8
+ * enumeration oracle)
9
+ * - a database failure must propagate rather than collapse into "disabled", so
10
+ * an outage is reported as an error instead of being silently reported as an
11
+ * owner's privacy choice
12
+ *
13
+ * Both services' findOneBy are spied on, so no database is touched.
14
+ */
15
+
16
+ import StatusPageService from "../../../Server/Services/StatusPageService";
17
+ import StatusPageDomainService from "../../../Server/Services/StatusPageDomainService";
18
+ import StatusPage from "../../../Models/DatabaseModels/StatusPage";
19
+ import StatusPageDomain from "../../../Models/DatabaseModels/StatusPageDomain";
20
+ import ObjectID from "../../../Types/ObjectID";
21
+ import { afterEach, describe, expect, test, jest } from "@jest/globals";
22
+
23
+ const VALID_UUID: string = "550e8400-e29b-41d4-a716-446655440000";
24
+ const DOMAIN: string = "status.company.com";
25
+
26
+ function statusPageDomainWith(statusPageId: ObjectID | null): StatusPageDomain {
27
+ return {
28
+ statusPageId: statusPageId ?? undefined,
29
+ } as unknown as StatusPageDomain;
30
+ }
31
+
32
+ function statusPageRow(): StatusPage {
33
+ return { _id: VALID_UUID } as unknown as StatusPage;
34
+ }
35
+
36
+ function spyOnStatusPageFindOneBy(): jest.SpyInstance {
37
+ return jest.spyOn(
38
+ StatusPageService,
39
+ "findOneBy",
40
+ ) as unknown as jest.SpyInstance;
41
+ }
42
+
43
+ function spyOnDomainFindOneBy(): jest.SpyInstance {
44
+ return jest.spyOn(
45
+ StatusPageDomainService,
46
+ "findOneBy",
47
+ ) as unknown as jest.SpyInstance;
48
+ }
49
+
50
+ describe("StatusPageService MCP gate", () => {
51
+ afterEach(() => {
52
+ jest.restoreAllMocks();
53
+ });
54
+
55
+ describe("resolveStatusPageIdOrNull", () => {
56
+ test("resolves a UUID without querying the domain table", async () => {
57
+ const domainSpy: jest.SpyInstance = spyOnDomainFindOneBy();
58
+
59
+ const result: ObjectID | null =
60
+ await StatusPageService.resolveStatusPageIdOrNull(VALID_UUID);
61
+
62
+ expect(result?.toString()).toBe(VALID_UUID);
63
+ expect(domainSpy).not.toHaveBeenCalled();
64
+ });
65
+
66
+ test("looks a domain up by fullDomain, requiring a verified domain", async () => {
67
+ const statusPageId: ObjectID = new ObjectID(VALID_UUID);
68
+ const domainSpy: jest.SpyInstance =
69
+ spyOnDomainFindOneBy().mockResolvedValue(
70
+ statusPageDomainWith(statusPageId) as never,
71
+ );
72
+
73
+ const result: ObjectID | null =
74
+ await StatusPageService.resolveStatusPageIdOrNull(DOMAIN);
75
+
76
+ expect(result?.toString()).toBe(VALID_UUID);
77
+
78
+ const args: Record<string, any> = domainSpy.mock.calls[0]![0] as Record<
79
+ string,
80
+ any
81
+ >;
82
+
83
+ expect(args["query"]["fullDomain"]).toBe(DOMAIN);
84
+ expect(args["query"]["domain"]).toEqual({ isVerified: true });
85
+ expect(args["props"]["isRoot"]).toBe(true);
86
+ });
87
+
88
+ test("returns null when the domain is not registered", async () => {
89
+ spyOnDomainFindOneBy().mockResolvedValue(null as never);
90
+
91
+ await expect(
92
+ StatusPageService.resolveStatusPageIdOrNull(DOMAIN),
93
+ ).resolves.toBeNull();
94
+ });
95
+
96
+ test("returns null when the domain row carries no status page", async () => {
97
+ spyOnDomainFindOneBy().mockResolvedValue(
98
+ statusPageDomainWith(null) as never,
99
+ );
100
+
101
+ await expect(
102
+ StatusPageService.resolveStatusPageIdOrNull(DOMAIN),
103
+ ).resolves.toBeNull();
104
+ });
105
+
106
+ test("returns null for an empty identifier without querying", async () => {
107
+ const domainSpy: jest.SpyInstance = spyOnDomainFindOneBy();
108
+
109
+ await expect(
110
+ StatusPageService.resolveStatusPageIdOrNull(""),
111
+ ).resolves.toBeNull();
112
+ expect(domainSpy).not.toHaveBeenCalled();
113
+ });
114
+
115
+ test("returns null for a malformed UUID rather than throwing", async () => {
116
+ await expect(
117
+ StatusPageService.resolveStatusPageIdOrNull("not-a-uuid"),
118
+ ).resolves.toBeNull();
119
+ });
120
+
121
+ test("returns null for a dotless hostname", async () => {
122
+ /*
123
+ * The dot is the discriminator, mirroring resolveStatusPageIdOrThrow in
124
+ * StatusPageAPI. "localhost" therefore goes down the UUID path and fails
125
+ * there. Pinned so the two resolvers cannot silently diverge.
126
+ */
127
+ await expect(
128
+ StatusPageService.resolveStatusPageIdOrNull("localhost"),
129
+ ).resolves.toBeNull();
130
+ });
131
+ });
132
+
133
+ describe("isMcpServerEnabled", () => {
134
+ test("is true when a UUID matches a status page with MCP enabled", async () => {
135
+ spyOnStatusPageFindOneBy().mockResolvedValue(statusPageRow() as never);
136
+
137
+ await expect(
138
+ StatusPageService.isMcpServerEnabled(VALID_UUID),
139
+ ).resolves.toBe(true);
140
+ });
141
+
142
+ test("is false when the status page has MCP disabled", async () => {
143
+ /*
144
+ * The enableMcpServer predicate is in the query, so a gated page simply
145
+ * does not match.
146
+ */
147
+ spyOnStatusPageFindOneBy().mockResolvedValue(null as never);
148
+
149
+ await expect(
150
+ StatusPageService.isMcpServerEnabled(VALID_UUID),
151
+ ).resolves.toBe(false);
152
+ });
153
+
154
+ test("folds the flag into the query and reads as root", async () => {
155
+ const spy: jest.SpyInstance =
156
+ spyOnStatusPageFindOneBy().mockResolvedValue(statusPageRow() as never);
157
+
158
+ await StatusPageService.isMcpServerEnabled(VALID_UUID);
159
+
160
+ const args: Record<string, any> = spy.mock.calls[0]![0] as Record<
161
+ string,
162
+ any
163
+ >;
164
+
165
+ expect(args["query"]["enableMcpServer"]).toBe(true);
166
+ expect(args["query"]["_id"].toString()).toBe(VALID_UUID);
167
+ expect(args["props"]["isRoot"]).toBe(true);
168
+ });
169
+
170
+ test("resolves a domain and then checks the flag", async () => {
171
+ spyOnDomainFindOneBy().mockResolvedValue(
172
+ statusPageDomainWith(new ObjectID(VALID_UUID)) as never,
173
+ );
174
+ const pageSpy: jest.SpyInstance =
175
+ spyOnStatusPageFindOneBy().mockResolvedValue(statusPageRow() as never);
176
+
177
+ await expect(StatusPageService.isMcpServerEnabled(DOMAIN)).resolves.toBe(
178
+ true,
179
+ );
180
+ expect(pageSpy).toHaveBeenCalledTimes(1);
181
+ });
182
+
183
+ test("is false for an unknown status page, without a second query", async () => {
184
+ spyOnDomainFindOneBy().mockResolvedValue(null as never);
185
+ const pageSpy: jest.SpyInstance = spyOnStatusPageFindOneBy();
186
+
187
+ await expect(StatusPageService.isMcpServerEnabled(DOMAIN)).resolves.toBe(
188
+ false,
189
+ );
190
+ expect(pageSpy).not.toHaveBeenCalled();
191
+ });
192
+
193
+ test("is false for an unresolvable identifier", async () => {
194
+ await expect(
195
+ StatusPageService.isMcpServerEnabled("not-a-uuid"),
196
+ ).resolves.toBe(false);
197
+ });
198
+
199
+ test("propagates a status page query failure instead of reporting disabled", async () => {
200
+ /*
201
+ * Fails closed either way, but the caller reports an error rather than
202
+ * telling the agent the owner turned MCP off.
203
+ */
204
+ spyOnStatusPageFindOneBy().mockRejectedValue(
205
+ new Error("connection terminated") as never,
206
+ );
207
+
208
+ await expect(
209
+ StatusPageService.isMcpServerEnabled(VALID_UUID),
210
+ ).rejects.toThrow("connection terminated");
211
+ });
212
+
213
+ test("propagates a domain query failure", async () => {
214
+ spyOnDomainFindOneBy().mockRejectedValue(
215
+ new Error("connection terminated") as never,
216
+ );
217
+
218
+ await expect(
219
+ StatusPageService.isMcpServerEnabled(DOMAIN),
220
+ ).rejects.toThrow("connection terminated");
221
+ });
222
+ });
223
+ });
@@ -1,7 +1,8 @@
1
- import TelemetryExceptionService, {
1
+ import TelemetryExceptionService from "../../../Server/Services/TelemetryExceptionService";
2
+ import {
2
3
  AIFixReadiness,
3
4
  AIFixReadinessCheck,
4
- } from "../../../Server/Services/TelemetryExceptionService";
5
+ } from "../../../Types/AI/AIFixReadiness";
5
6
  import FixRunBudget from "../../../Server/Utils/AI/CodeFix/FixRunBudget";
6
7
  import LlmProviderService from "../../../Server/Services/LlmProviderService";
7
8
  import ProjectService from "../../../Server/Services/ProjectService";
@@ -19,7 +20,8 @@ import AIAgent, {
19
20
  import BadDataException from "../../../Types/Exception/BadDataException";
20
21
  import ObjectID from "../../../Types/ObjectID";
21
22
  import OneUptimeDate from "../../../Types/Date";
22
- import { describe, expect, test, afterEach } from "@jest/globals";
23
+ import AIService from "../../../Server/Services/AIService";
24
+ import { describe, expect, test, afterEach, beforeEach } from "@jest/globals";
23
25
 
24
26
  /*
25
27
  * "Fix with AI Agent" readiness: every prerequisite is checked BEFORE a task
@@ -121,7 +123,24 @@ function getCheck(readiness: AIFixReadiness, id: string): AIFixReadinessCheck {
121
123
  return check!;
122
124
  }
123
125
 
126
+ /*
127
+ * The llmProvider check also gates on the daily autonomous token budget
128
+ * (AI_CODE_FIX_FEATURE is autonomous, so executeWithLogging enforces it).
129
+ * Default it to unset — these suites are about the other fail conditions.
130
+ */
131
+ function mockBudgetNotExhausted(): void {
132
+ jest.spyOn(AIService, "getAutonomousDailyBudgetStatus").mockResolvedValue({
133
+ exhausted: false,
134
+ limitInTokens: null,
135
+ usedTokensToday: 0,
136
+ });
137
+ }
138
+
124
139
  describe("TelemetryExceptionService.getAIFixReadiness", () => {
140
+ beforeEach(() => {
141
+ mockBudgetNotExhausted();
142
+ });
143
+
125
144
  afterEach(() => {
126
145
  jest.restoreAllMocks();
127
146
  });
@@ -237,6 +256,10 @@ describe("TelemetryExceptionService.getAIFixReadiness", () => {
237
256
  * and free-global providers consume no balance and must never require one.
238
257
  */
239
258
  describe("TelemetryExceptionService.getAIFixReadiness — AI balance gate", () => {
259
+ beforeEach(() => {
260
+ mockBudgetNotExhausted();
261
+ });
262
+
240
263
  afterEach(() => {
241
264
  jest.restoreAllMocks();
242
265
  });
@@ -1,4 +1,5 @@
1
1
  import TelemetryExceptionService from "../../../Server/Services/TelemetryExceptionService";
2
+ import AIService from "../../../Server/Services/AIService";
2
3
  import FixRunBudget from "../../../Server/Utils/AI/CodeFix/FixRunBudget";
3
4
  import LlmProviderService from "../../../Server/Services/LlmProviderService";
4
5
  import ServiceService from "../../../Server/Services/ServiceService";
@@ -79,6 +80,17 @@ function mockReadinessOk(): void {
79
80
  name: "agent",
80
81
  connectionStatus: AIAgentConnectionStatus.Connected,
81
82
  } as unknown as AIAgent);
83
+ /*
84
+ * The llmProvider check also gates on the daily autonomous token budget
85
+ * (AI_CODE_FIX_FEATURE is autonomous, so executeWithLogging enforces it).
86
+ * Default it to unset — this suite is about the duplicate guard, and the
87
+ * budget has its own suite (AIServiceDailyBudget.test.ts).
88
+ */
89
+ jest.spyOn(AIService, "getAutonomousDailyBudgetStatus").mockResolvedValue({
90
+ exhausted: false,
91
+ limitInTokens: null,
92
+ usedTokensToday: 0,
93
+ });
82
94
  }
83
95
 
84
96
  describe("TelemetryExceptionService.createCodeFixRunForException", () => {
@@ -0,0 +1,106 @@
1
+ import Markdown from "../../../Server/Types/Markdown";
2
+
3
+ /*
4
+ * slugify turns a heading into the `id` the docs renderer emits, and therefore
5
+ * into the target every in-page `](#anchor)` link has to match. These tests pin
6
+ * the two properties that are easy to break by accident:
7
+ *
8
+ * - non-ASCII scripts must survive (they used to be stripped to an empty id,
9
+ * which silently 404'd every anchor in the ru/ja/ko/hi/zh docs), and
10
+ * - existing English ids must not move, because they are published URLs.
11
+ */
12
+ describe("Markdown.slugify", () => {
13
+ describe("English headings keep their existing ids", () => {
14
+ test.each([
15
+ [
16
+ "Reducing the Volume of Data Collected",
17
+ "reducing-the-volume-of-data-collected",
18
+ ],
19
+ ["Filtering by Log Severity", "filtering-by-log-severity"],
20
+ ["Step 1 — Install the Collector", "step-1-install-the-collector"],
21
+ ["What's New?", "whats-new"],
22
+ ["Namespace Filtering", "namespace-filtering"],
23
+ ])("%s", (heading: string, expected: string) => {
24
+ expect(Markdown.slugify(heading)).toBe(expected);
25
+ });
26
+ });
27
+
28
+ /*
29
+ * \p{L}/\p{N} do not match "_", so it is listed explicitly in the character
30
+ * class. The old `\w` matched it. These are real headings — dropping the
31
+ * underscore would change ids that are already linked to.
32
+ */
33
+ describe("underscores are preserved", () => {
34
+ test.each([
35
+ [
36
+ "`ceph_health_status` is 1 but no incident fires",
37
+ "ceph_health_status-is-1-but-no-incident-fires",
38
+ ],
39
+ [
40
+ "Counters like `pve_network_receive_bytes` only ever grow",
41
+ "counters-like-pve_network_receive_bytes-only-ever-grow",
42
+ ],
43
+ ])("%s", (heading: string, expected: string) => {
44
+ expect(Markdown.slugify(heading)).toBe(expected);
45
+ });
46
+ });
47
+
48
+ describe("non-Latin scripts produce a usable id, not an empty one", () => {
49
+ test.each([
50
+ ["Фильтрация по важности логов", "фильтрация-по-важности-логов"],
51
+ ["ログの重大度によるフィルタリング", "ログの重大度によるフィルタリング"],
52
+ ["按日志严重性过滤", "按日志严重性过滤"],
53
+ ["로그 심각도별 필터링", "로그-심각도별-필터링"],
54
+ [
55
+ "लॉग गंभीरता के अनुसार फ़िल्टर करना",
56
+ "लॉग-गंभीरता-के-अनुसार-फ़िल्टर-करना",
57
+ ],
58
+ ])("%s", (heading: string, expected: string) => {
59
+ const slug: string = Markdown.slugify(heading);
60
+ expect(slug).not.toBe("");
61
+ expect(slug).toBe(expected);
62
+ });
63
+ });
64
+
65
+ describe("accented Latin keeps the accented letter", () => {
66
+ test.each([
67
+ [
68
+ "Filtrage par gravité des journaux",
69
+ "filtrage-par-gravité-des-journaux",
70
+ ],
71
+ [
72
+ "Reduktion af mængden af indsamlede data",
73
+ "reduktion-af-mængden-af-indsamlede-data",
74
+ ],
75
+ ["Fehlerbehebung für Größen", "fehlerbehebung-für-größen"],
76
+ ])("%s", (heading: string, expected: string) => {
77
+ expect(Markdown.slugify(heading)).toBe(expected);
78
+ });
79
+ });
80
+
81
+ describe("markup and punctuation are still stripped", () => {
82
+ test("strips inline HTML tags", () => {
83
+ expect(Markdown.slugify("A <em>heading</em> here")).toBe(
84
+ "a-heading-here",
85
+ );
86
+ });
87
+
88
+ test("strips HTML entities", () => {
89
+ expect(Markdown.slugify("Tom &amp; Jerry")).toBe("tom-jerry");
90
+ });
91
+
92
+ test("strips punctuation but keeps hyphens", () => {
93
+ expect(Markdown.slugify("Set up SSO (SAML): step-by-step!")).toBe(
94
+ "set-up-sso-saml-step-by-step",
95
+ );
96
+ });
97
+
98
+ test("collapses whitespace runs and trims leading/trailing hyphens", () => {
99
+ expect(Markdown.slugify(" spaced out ")).toBe("spaced-out");
100
+ });
101
+
102
+ test("emoji are dropped rather than becoming part of the id", () => {
103
+ expect(Markdown.slugify("🚀 Getting Started")).toBe("getting-started");
104
+ });
105
+ });
106
+ });
@@ -0,0 +1,182 @@
1
+ import AIRunTranscript, {
2
+ MAX_MESSAGE_CONTENT_CHARS,
3
+ MAX_PAYLOAD_CHARS,
4
+ MAX_RESPONSE_CONTENT_CHARS,
5
+ MAX_TOOL_ARGUMENT_CHARS,
6
+ MAX_TOOL_RESULT_CHARS,
7
+ } from "../../../../Server/Utils/AI/AIRunTranscript";
8
+ import { AIRunEventContentPayload } from "../../../../Types/AI/AIChatTypes";
9
+ import { describe, expect, test } from "@jest/globals";
10
+
11
+ /*
12
+ * The run transcript is the ONE place a code-fix run's LLM content is stored
13
+ * (LlmLog redacts it — it is project-wide readable and these prompts embed
14
+ * customer source code). It stores content verbatim to be worth reading when
15
+ * a run goes wrong, which makes bounding it a storage requirement rather than
16
+ * a nicety: an agent may read large files and run test suites emitting
17
+ * megabytes, up to 40 times per run.
18
+ *
19
+ * These tests pin the two properties that make that safe: every field is
20
+ * capped, and the payload as a whole cannot exceed MAX_PAYLOAD_CHARS no
21
+ * matter WHICH field carries the bulk.
22
+ */
23
+
24
+ type SerializedLengthFunction = (payload: AIRunEventContentPayload) => number;
25
+
26
+ const serializedLength: SerializedLengthFunction = (
27
+ payload: AIRunEventContentPayload,
28
+ ): number => {
29
+ return JSON.stringify(payload).length;
30
+ };
31
+
32
+ describe("AIRunTranscript.clip", () => {
33
+ test("leaves a value under the cap untouched and unflagged", () => {
34
+ expect(AIRunTranscript.clip("short", 100)).toEqual({
35
+ value: "short",
36
+ isTruncated: false,
37
+ });
38
+ });
39
+
40
+ test("clips an oversized value and reports the truncation", () => {
41
+ const result: { value: string | undefined; isTruncated: boolean } =
42
+ AIRunTranscript.clip("x".repeat(50), 10);
43
+
44
+ expect(result.isTruncated).toBe(true);
45
+ expect(result.value).toContain("truncated");
46
+ expect(result.value!.startsWith("x".repeat(10))).toBe(true);
47
+ });
48
+
49
+ test("passes undefined through without inventing a value", () => {
50
+ expect(AIRunTranscript.clip(undefined, 10)).toEqual({
51
+ value: undefined,
52
+ isTruncated: false,
53
+ });
54
+ });
55
+ });
56
+
57
+ describe("AIRunTranscript.clipArguments", () => {
58
+ test("keeps arguments under the cap verbatim", () => {
59
+ const args: { path: string } = { path: "src/Index.ts" };
60
+ expect(AIRunTranscript.clipArguments(args)).toEqual({
61
+ value: args,
62
+ isTruncated: false,
63
+ });
64
+ });
65
+
66
+ /*
67
+ * A write_file call carries a whole file in its arguments — the single
68
+ * likeliest way one tool call bloats a row.
69
+ */
70
+ test("replaces oversized arguments with a marker that keeps a preview", () => {
71
+ const result: {
72
+ value: Record<string, unknown> | undefined;
73
+ isTruncated: boolean;
74
+ } = AIRunTranscript.clipArguments({
75
+ content: "y".repeat(MAX_TOOL_ARGUMENT_CHARS + 1000),
76
+ });
77
+
78
+ expect(result.isTruncated).toBe(true);
79
+ expect(result.value!["_note"]).toContain("exceeds");
80
+ expect(typeof result.value!["_preview"]).toBe("string");
81
+ });
82
+ });
83
+
84
+ describe("AIRunTranscript.clampPayload", () => {
85
+ test("caps each field independently and stamps isTruncated", () => {
86
+ const clamped: AIRunEventContentPayload = AIRunTranscript.clampPayload({
87
+ requestMessages: [
88
+ { role: "user", content: "a".repeat(MAX_MESSAGE_CONTENT_CHARS + 500) },
89
+ ],
90
+ responseContent: "b".repeat(MAX_RESPONSE_CONTENT_CHARS + 500),
91
+ toolResult: "c".repeat(MAX_TOOL_RESULT_CHARS + 500),
92
+ });
93
+
94
+ expect(clamped.isTruncated).toBe(true);
95
+ expect(clamped.requestMessages![0]!.content.length).toBeLessThanOrEqual(
96
+ MAX_MESSAGE_CONTENT_CHARS + 100,
97
+ );
98
+ expect(clamped.responseContent!.length).toBeLessThanOrEqual(
99
+ MAX_RESPONSE_CONTENT_CHARS + 100,
100
+ );
101
+ expect(clamped.toolResult!.length).toBeLessThanOrEqual(
102
+ MAX_TOOL_RESULT_CHARS + 100,
103
+ );
104
+ });
105
+
106
+ test("leaves a small payload entirely alone", () => {
107
+ const payload: AIRunEventContentPayload = {
108
+ requestMessages: [{ role: "user", content: "fix the null deref" }],
109
+ responseContent: "Reading the file.",
110
+ responseToolCalls: [{ name: "read_file", arguments: { path: "a.ts" } }],
111
+ totalTokens: 120,
112
+ };
113
+
114
+ const clamped: AIRunEventContentPayload =
115
+ AIRunTranscript.clampPayload(payload);
116
+
117
+ expect(clamped.isTruncated).toBeUndefined();
118
+ expect(clamped).toEqual(payload);
119
+ });
120
+
121
+ /*
122
+ * The regression this file exists for. A single-pass backstop that only
123
+ * dropped requestMessages "succeeded" by deleting a two-character user
124
+ * message while the row stayed ~1.5x over the ceiling, because the bulk was
125
+ * in responseToolCalls. The cap must hold regardless of which field is fat.
126
+ */
127
+ test("holds the ceiling when the bulk is in responseToolCalls, not messages", () => {
128
+ const clamped: AIRunEventContentPayload = AIRunTranscript.clampPayload({
129
+ requestMessages: [{ role: "user", content: "hi" }],
130
+ responseToolCalls: Array.from(
131
+ { length: 30 },
132
+ (_value: unknown, index: number) => {
133
+ return {
134
+ id: `call_${index}`,
135
+ name: "write_file",
136
+ arguments: { content: "z".repeat(MAX_TOOL_ARGUMENT_CHARS - 200) },
137
+ };
138
+ },
139
+ ),
140
+ });
141
+
142
+ expect(serializedLength(clamped)).toBeLessThanOrEqual(MAX_PAYLOAD_CHARS);
143
+ expect(clamped.isTruncated).toBe(true);
144
+ });
145
+
146
+ test("holds the ceiling when the bulk is a huge tool result", () => {
147
+ const clamped: AIRunEventContentPayload = AIRunTranscript.clampPayload({
148
+ toolResult: "q".repeat(MAX_PAYLOAD_CHARS * 3),
149
+ });
150
+
151
+ expect(serializedLength(clamped)).toBeLessThanOrEqual(MAX_PAYLOAD_CHARS);
152
+ expect(clamped.isTruncated).toBe(true);
153
+ });
154
+
155
+ /*
156
+ * Bounded ENTRIES are not a bounded ARRAY: with every argument already
157
+ * dropped, enough calls still blow the ceiling. The last resort collapses
158
+ * the array to a count.
159
+ */
160
+ test("holds the ceiling against a pathological number of tool calls", () => {
161
+ const clamped: AIRunEventContentPayload = AIRunTranscript.clampPayload({
162
+ responseToolCalls: Array.from(
163
+ { length: 20000 },
164
+ (_value: unknown, index: number) => {
165
+ return { id: `call_${index}`, name: "read_file" };
166
+ },
167
+ ),
168
+ });
169
+
170
+ expect(serializedLength(clamped)).toBeLessThanOrEqual(MAX_PAYLOAD_CHARS);
171
+ expect(clamped.isTruncated).toBe(true);
172
+ });
173
+
174
+ // A clipped prompt must never be presented as though it were whole.
175
+ test("never reports a clipped payload as complete", () => {
176
+ const clamped: AIRunEventContentPayload = AIRunTranscript.clampPayload({
177
+ responseContent: "r".repeat(MAX_RESPONSE_CONTENT_CHARS + 1),
178
+ });
179
+
180
+ expect(clamped.isTruncated).toBe(true);
181
+ });
182
+ });