@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,411 @@
1
+ import CodeFixReadiness from "../../../../Server/Utils/AI/CodeFix/CodeFixReadiness";
2
+ import SubjectCodeFixRun from "../../../../Server/Utils/AI/SRE/SubjectCodeFixRun";
3
+ import AIAgentService from "../../../../Server/Services/AIAgentService";
4
+ import AIService from "../../../../Server/Services/AIService";
5
+ import LlmProviderService from "../../../../Server/Services/LlmProviderService";
6
+ import ProjectService from "../../../../Server/Services/ProjectService";
7
+ import AIAgent from "../../../../Models/DatabaseModels/AIAgent";
8
+ import LlmProvider from "../../../../Models/DatabaseModels/LlmProvider";
9
+ import Project from "../../../../Models/DatabaseModels/Project";
10
+ import ObjectID from "../../../../Types/ObjectID";
11
+ import {
12
+ AIFixReadiness,
13
+ AIFixReadinessCheck,
14
+ } from "../../../../Types/AI/AIFixReadiness";
15
+ import { describe, expect, test, afterEach, beforeEach } from "@jest/globals";
16
+
17
+ /*
18
+ * The gates the AI Tasks page renders. The invariant worth protecting: a
19
+ * project LLM provider is NOT a prerequisite — the shared global provider is
20
+ * a legitimate resolution for the agent path, so a project running on it must
21
+ * read as ready. The banner these checks replaced claimed otherwise.
22
+ */
23
+
24
+ const projectId: ObjectID = ObjectID.generate();
25
+
26
+ function fakeProvider(params: {
27
+ name: string;
28
+ isGlobalLlm?: boolean;
29
+ costPerMillionTokensInUSDCents?: number;
30
+ }): LlmProvider {
31
+ return {
32
+ id: ObjectID.generate(),
33
+ name: params.name,
34
+ isGlobalLlm: params.isGlobalLlm ?? false,
35
+ costPerMillionTokensInUSDCents: params.costPerMillionTokensInUSDCents ?? 0,
36
+ } as unknown as LlmProvider;
37
+ }
38
+
39
+ // No daily-budget limit configured: the common case, and never a blocker.
40
+ function mockBudgetNotExhausted(): void {
41
+ jest.spyOn(AIService, "getAutonomousDailyBudgetStatus").mockResolvedValue({
42
+ exhausted: false,
43
+ limitInTokens: null,
44
+ usedTokensToday: 0,
45
+ });
46
+ }
47
+
48
+ describe("CodeFixReadiness.getLlmProviderCheck", () => {
49
+ beforeEach(() => {
50
+ mockBudgetNotExhausted();
51
+ });
52
+
53
+ afterEach(() => {
54
+ jest.restoreAllMocks();
55
+ });
56
+
57
+ test("no provider resolves at all: not ready, and says where to add one", async () => {
58
+ jest
59
+ .spyOn(LlmProviderService, "getLlmProviderForMeteredAgentPath")
60
+ .mockResolvedValue(null);
61
+
62
+ const check: AIFixReadinessCheck =
63
+ await CodeFixReadiness.getLlmProviderCheck({ projectId });
64
+
65
+ expect(check.id).toBe("llmProvider");
66
+ expect(check.ok).toBe(false);
67
+ expect(check.detail).toContain("LLM Providers");
68
+ });
69
+
70
+ test("a project-owned provider is ready and is named as the project's own", async () => {
71
+ jest
72
+ .spyOn(LlmProviderService, "getLlmProviderForMeteredAgentPath")
73
+ .mockResolvedValue(fakeProvider({ name: "My OpenAI" }));
74
+
75
+ const check: AIFixReadinessCheck =
76
+ await CodeFixReadiness.getLlmProviderCheck({
77
+ projectId,
78
+ billingEnabled: true,
79
+ });
80
+
81
+ expect(check.ok).toBe(true);
82
+ expect(check.title).toContain("My OpenAI");
83
+ expect(check.detail).toContain("this project's own provider");
84
+ });
85
+
86
+ /*
87
+ * The headline case the old banner got wrong: no project provider, a free
88
+ * global one, and the project is nonetheless ready.
89
+ */
90
+ test("a FREE global provider is ready with no project provider and no balance", async () => {
91
+ jest
92
+ .spyOn(LlmProviderService, "getLlmProviderForMeteredAgentPath")
93
+ .mockResolvedValue(
94
+ fakeProvider({
95
+ name: "Global LLM Provider",
96
+ isGlobalLlm: true,
97
+ costPerMillionTokensInUSDCents: 0,
98
+ }),
99
+ );
100
+ const projectSpy: jest.SpiedFunction<typeof ProjectService.findOneById> =
101
+ jest.spyOn(ProjectService, "findOneById");
102
+
103
+ const check: AIFixReadinessCheck =
104
+ await CodeFixReadiness.getLlmProviderCheck({
105
+ projectId,
106
+ billingEnabled: true,
107
+ });
108
+
109
+ expect(check.ok).toBe(true);
110
+ expect(check.detail).toContain("shared provider");
111
+ // A free provider bills nothing, so the balance must never be consulted.
112
+ expect(projectSpy).not.toHaveBeenCalled();
113
+ });
114
+
115
+ test("a costed global provider with billing OFF is ready and skips the balance check", async () => {
116
+ jest
117
+ .spyOn(LlmProviderService, "getLlmProviderForMeteredAgentPath")
118
+ .mockResolvedValue(
119
+ fakeProvider({
120
+ name: "OneUptime AI",
121
+ isGlobalLlm: true,
122
+ costPerMillionTokensInUSDCents: 500,
123
+ }),
124
+ );
125
+ const projectSpy: jest.SpiedFunction<typeof ProjectService.findOneById> =
126
+ jest.spyOn(ProjectService, "findOneById");
127
+
128
+ const check: AIFixReadinessCheck =
129
+ await CodeFixReadiness.getLlmProviderCheck({
130
+ projectId,
131
+ billingEnabled: false,
132
+ });
133
+
134
+ expect(check.ok).toBe(true);
135
+ expect(projectSpy).not.toHaveBeenCalled();
136
+ });
137
+
138
+ test("a costed global provider with billing ON and a funded balance is ready and says it is metered", async () => {
139
+ jest
140
+ .spyOn(LlmProviderService, "getLlmProviderForMeteredAgentPath")
141
+ .mockResolvedValue(
142
+ fakeProvider({
143
+ name: "OneUptime AI",
144
+ isGlobalLlm: true,
145
+ costPerMillionTokensInUSDCents: 500,
146
+ }),
147
+ );
148
+ jest.spyOn(ProjectService, "findOneById").mockResolvedValue({
149
+ aiCurrentBalanceInUSDCents: 2000,
150
+ } as unknown as Project);
151
+
152
+ const check: AIFixReadinessCheck =
153
+ await CodeFixReadiness.getLlmProviderCheck({
154
+ projectId,
155
+ billingEnabled: true,
156
+ });
157
+
158
+ expect(check.ok).toBe(true);
159
+ expect(check.detail).toContain("AI balance");
160
+ });
161
+
162
+ test("a costed global provider with billing ON and an empty balance is NOT ready", async () => {
163
+ jest
164
+ .spyOn(LlmProviderService, "getLlmProviderForMeteredAgentPath")
165
+ .mockResolvedValue(
166
+ fakeProvider({
167
+ name: "OneUptime AI",
168
+ isGlobalLlm: true,
169
+ costPerMillionTokensInUSDCents: 500,
170
+ }),
171
+ );
172
+ jest.spyOn(ProjectService, "findOneById").mockResolvedValue({
173
+ aiCurrentBalanceInUSDCents: 0,
174
+ } as unknown as Project);
175
+
176
+ const check: AIFixReadinessCheck =
177
+ await CodeFixReadiness.getLlmProviderCheck({
178
+ projectId,
179
+ billingEnabled: true,
180
+ });
181
+
182
+ expect(check.ok).toBe(false);
183
+ expect(check.detail).toContain("AI Credits");
184
+ });
185
+ });
186
+
187
+ /*
188
+ * AI_CODE_FIX_FEATURE is one of AUTONOMOUS_AI_FEATURES, so executeWithLogging
189
+ * runs every fix completion through the daily token budget. Readiness that
190
+ * ignored it would report ready and then die at the run's first completion
191
+ * call — the exact fail-late hole the balance gate exists to close.
192
+ */
193
+ describe("CodeFixReadiness.getLlmProviderCheck — daily autonomous token budget", () => {
194
+ afterEach(() => {
195
+ jest.restoreAllMocks();
196
+ });
197
+
198
+ test("a zeroed daily limit (the kill switch) is NOT ready, even with a healthy project-owned provider", async () => {
199
+ jest
200
+ .spyOn(LlmProviderService, "getLlmProviderForMeteredAgentPath")
201
+ .mockResolvedValue(fakeProvider({ name: "My OpenAI" }));
202
+ jest.spyOn(AIService, "getAutonomousDailyBudgetStatus").mockResolvedValue({
203
+ exhausted: true,
204
+ limitInTokens: 0,
205
+ usedTokensToday: 0,
206
+ });
207
+
208
+ const check: AIFixReadinessCheck =
209
+ await CodeFixReadiness.getLlmProviderCheck({
210
+ projectId,
211
+ billingEnabled: false,
212
+ });
213
+
214
+ expect(check.ok).toBe(false);
215
+ expect(check.detail).toContain("paused");
216
+ });
217
+
218
+ test("an exhausted daily budget is NOT ready and reports the usage", async () => {
219
+ jest
220
+ .spyOn(LlmProviderService, "getLlmProviderForMeteredAgentPath")
221
+ .mockResolvedValue(fakeProvider({ name: "My OpenAI" }));
222
+ jest.spyOn(AIService, "getAutonomousDailyBudgetStatus").mockResolvedValue({
223
+ exhausted: true,
224
+ limitInTokens: 1000,
225
+ usedTokensToday: 1000,
226
+ });
227
+
228
+ const check: AIFixReadinessCheck =
229
+ await CodeFixReadiness.getLlmProviderCheck({
230
+ projectId,
231
+ billingEnabled: false,
232
+ });
233
+
234
+ expect(check.ok).toBe(false);
235
+ expect(check.detail).toContain("exhausted");
236
+ expect(check.detail).toContain("1,000");
237
+ });
238
+
239
+ test("an unset daily limit never blocks", async () => {
240
+ jest
241
+ .spyOn(LlmProviderService, "getLlmProviderForMeteredAgentPath")
242
+ .mockResolvedValue(fakeProvider({ name: "My OpenAI" }));
243
+ mockBudgetNotExhausted();
244
+
245
+ const check: AIFixReadinessCheck =
246
+ await CodeFixReadiness.getLlmProviderCheck({
247
+ projectId,
248
+ billingEnabled: false,
249
+ });
250
+
251
+ expect(check.ok).toBe(true);
252
+ });
253
+ });
254
+
255
+ describe("CodeFixReadiness.getAgentCheck", () => {
256
+ afterEach(() => {
257
+ jest.restoreAllMocks();
258
+ });
259
+
260
+ test("no agent for the project: not ready", async () => {
261
+ jest.spyOn(AIAgentService, "getAIAgentForProject").mockResolvedValue(null);
262
+
263
+ const check: AIFixReadinessCheck = await CodeFixReadiness.getAgentCheck({
264
+ projectId,
265
+ });
266
+
267
+ expect(check.id).toBe("agentAvailable");
268
+ expect(check.ok).toBe(false);
269
+ expect(check.detail).toContain("No AI agent is available");
270
+ });
271
+
272
+ test("an agent that exists but is not alive: not ready, and names it", async () => {
273
+ jest
274
+ .spyOn(AIAgentService, "getAIAgentForProject")
275
+ .mockResolvedValue({ name: "fix-worker" } as unknown as AIAgent);
276
+ jest.spyOn(AIAgentService, "isAgentAlive").mockReturnValue(false);
277
+
278
+ const check: AIFixReadinessCheck = await CodeFixReadiness.getAgentCheck({
279
+ projectId,
280
+ });
281
+
282
+ expect(check.ok).toBe(false);
283
+ expect(check.detail).toContain("fix-worker");
284
+ });
285
+
286
+ test("a live agent is ready and is named", async () => {
287
+ jest
288
+ .spyOn(AIAgentService, "getAIAgentForProject")
289
+ .mockResolvedValue({ name: "fix-worker" } as unknown as AIAgent);
290
+ jest.spyOn(AIAgentService, "isAgentAlive").mockReturnValue(true);
291
+
292
+ const check: AIFixReadinessCheck = await CodeFixReadiness.getAgentCheck({
293
+ projectId,
294
+ });
295
+
296
+ expect(check.ok).toBe(true);
297
+ expect(check.title).toContain("fix-worker");
298
+ });
299
+ });
300
+
301
+ describe("CodeFixReadiness.getRepositoryConnectedCheck", () => {
302
+ afterEach(() => {
303
+ jest.restoreAllMocks();
304
+ });
305
+
306
+ /*
307
+ * The page-level gate asserts the WEAKER any-repo claim, so its id must
308
+ * stay distinct from the per-exception repositoryResolved — a UI that saw
309
+ * the same id would imply a specific exception will resolve.
310
+ */
311
+ test("a connected repository is ready under the repositoryConnected id", async () => {
312
+ jest
313
+ .spyOn(SubjectCodeFixRun, "hasGitHubAppConnectedRepository")
314
+ .mockResolvedValue(true);
315
+
316
+ const check: AIFixReadinessCheck =
317
+ await CodeFixReadiness.getRepositoryConnectedCheck({ projectId });
318
+
319
+ expect(check.id).toBe("repositoryConnected");
320
+ expect(check.ok).toBe(true);
321
+ });
322
+
323
+ test("no connected repository: not ready, and points at the GitHub App", async () => {
324
+ jest
325
+ .spyOn(SubjectCodeFixRun, "hasGitHubAppConnectedRepository")
326
+ .mockResolvedValue(false);
327
+
328
+ const check: AIFixReadinessCheck =
329
+ await CodeFixReadiness.getRepositoryConnectedCheck({ projectId });
330
+
331
+ expect(check.ok).toBe(false);
332
+ expect(check.detail).toContain("GitHub App");
333
+ });
334
+ });
335
+
336
+ describe("CodeFixReadiness.getProjectReadiness", () => {
337
+ afterEach(() => {
338
+ jest.restoreAllMocks();
339
+ });
340
+
341
+ type MockAllParams = {
342
+ hasRepo: boolean;
343
+ hasProvider: boolean;
344
+ agentAlive: boolean;
345
+ };
346
+
347
+ function mockAll(params: MockAllParams): void {
348
+ mockBudgetNotExhausted();
349
+ jest
350
+ .spyOn(SubjectCodeFixRun, "hasGitHubAppConnectedRepository")
351
+ .mockResolvedValue(params.hasRepo);
352
+ jest
353
+ .spyOn(LlmProviderService, "getLlmProviderForMeteredAgentPath")
354
+ .mockResolvedValue(
355
+ params.hasProvider ? fakeProvider({ name: "provider" }) : null,
356
+ );
357
+ jest
358
+ .spyOn(AIAgentService, "getAIAgentForProject")
359
+ .mockResolvedValue({ name: "agent" } as unknown as AIAgent);
360
+ jest
361
+ .spyOn(AIAgentService, "isAgentAlive")
362
+ .mockReturnValue(params.agentAlive);
363
+ }
364
+
365
+ test("all three gates satisfied: ready", async () => {
366
+ mockAll({ hasRepo: true, hasProvider: true, agentAlive: true });
367
+
368
+ const readiness: AIFixReadiness =
369
+ await CodeFixReadiness.getProjectReadiness({ projectId });
370
+
371
+ expect(readiness.ready).toBe(true);
372
+ expect(readiness.checks).toHaveLength(3);
373
+ });
374
+
375
+ test("the checks are ordered the way the user sets them up", async () => {
376
+ mockAll({ hasRepo: true, hasProvider: true, agentAlive: true });
377
+
378
+ const readiness: AIFixReadiness =
379
+ await CodeFixReadiness.getProjectReadiness({ projectId });
380
+
381
+ expect(
382
+ readiness.checks.map((check: AIFixReadinessCheck) => {
383
+ return check.id;
384
+ }),
385
+ ).toEqual(["repositoryConnected", "llmProvider", "agentAvailable"]);
386
+ });
387
+
388
+ test("any single failing gate makes the whole project not ready", async () => {
389
+ const cases: Array<MockAllParams> = [
390
+ { hasRepo: false, hasProvider: true, agentAlive: true },
391
+ { hasRepo: true, hasProvider: false, agentAlive: true },
392
+ { hasRepo: true, hasProvider: true, agentAlive: false },
393
+ ];
394
+
395
+ for (const params of cases) {
396
+ jest.restoreAllMocks();
397
+ mockAll(params);
398
+
399
+ const readiness: AIFixReadiness =
400
+ await CodeFixReadiness.getProjectReadiness({ projectId });
401
+
402
+ expect(readiness.ready).toBe(false);
403
+ // The failing gate must still be reported, not swallowed.
404
+ expect(
405
+ readiness.checks.some((check: AIFixReadinessCheck) => {
406
+ return !check.ok;
407
+ }),
408
+ ).toBe(true);
409
+ }
410
+ });
411
+ });