@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,475 @@
1
+ /*
2
+ * Index MUST be imported before CodeWriteTools. The toolbox sits in an import
3
+ * cycle (tool -> service -> ... -> ObservabilityAssistant -> AIToolbox), so
4
+ * loading a tool module first re-enters Index before its tools array is
5
+ * assigned and leaves undefined holes in it. Production always enters through
6
+ * Index (ChatAgentRunner -> AIToolbox), which is the order reproduced here;
7
+ * the integrity test below fails loudly if that ever stops holding.
8
+ */
9
+ import AIToolbox from "../../../../Server/Utils/AI/Toolbox/Index";
10
+ import {
11
+ CommitCodeToBranchTool,
12
+ OpenCodePullRequestTool,
13
+ } from "../../../../Server/Utils/AI/Toolbox/CodeWriteTools";
14
+ import {
15
+ ToolContext,
16
+ ToolExecutionResult,
17
+ } from "../../../../Server/Utils/AI/Toolbox/ToolTypes";
18
+ import CodeRepositoryService from "../../../../Server/Services/CodeRepositoryService";
19
+ import AIAgentTaskPullRequestService from "../../../../Server/Services/AIAgentTaskPullRequestService";
20
+ import GitHubUtil from "../../../../Server/Utils/CodeRepository/GitHub/GitHub";
21
+ import CodeRepositoryModel from "../../../../Models/DatabaseModels/CodeRepository";
22
+ import AIAgentTaskPullRequest from "../../../../Models/DatabaseModels/AIAgentTaskPullRequest";
23
+ import CodeRepositoryType from "../../../../Types/CodeRepository/CodeRepositoryType";
24
+ import PullRequestState from "../../../../Types/CodeRepository/PullRequestState";
25
+ import ObjectID from "../../../../Types/ObjectID";
26
+ import { describe, expect, test, afterEach, beforeEach } from "@jest/globals";
27
+
28
+ /*
29
+ * The chat agent reads telemetry an attacker can influence (a log line or an
30
+ * exception message is whatever the monitored app threw) and can now turn it
31
+ * into code. AISentinelVision.md's defense is structural: a human reviews
32
+ * before anything merges.
33
+ *
34
+ * That defense reduces to ONE invariant, and these tests exist to make it
35
+ * impossible to regress silently:
36
+ *
37
+ * A chat-authored commit NEVER lands on the default branch, and never on a
38
+ * protected branch.
39
+ *
40
+ * Everything else here (caps, validation) is secondary to that.
41
+ */
42
+
43
+ const ctx: ToolContext = {
44
+ projectId: ObjectID.generate(),
45
+ props: { isRoot: true, userId: ObjectID.generate() },
46
+ };
47
+
48
+ const REPO_ID: ObjectID = ObjectID.generate();
49
+
50
+ function buildRepository(): CodeRepositoryModel {
51
+ const repository: CodeRepositoryModel = new CodeRepositoryModel();
52
+ repository._id = REPO_ID.toString();
53
+ repository.name = "checkout";
54
+ repository.organizationName = "acme";
55
+ repository.repositoryName = "checkout";
56
+ repository.mainBranchName = "main";
57
+ repository.repositoryHostedAt = CodeRepositoryType.GitHub;
58
+ repository.gitHubAppInstallationId = "12345";
59
+ return repository;
60
+ }
61
+
62
+ const CHANGES: Array<{ filePath: string; content: string }> = [
63
+ { filePath: "src/billing/charge.ts", content: "export const a = 1;\n" },
64
+ ];
65
+
66
+ beforeEach(() => {
67
+ jest
68
+ .spyOn(CodeRepositoryService, "findBy")
69
+ .mockResolvedValue([buildRepository()] as never);
70
+ jest.spyOn(GitHubUtil, "getDefaultBranchName").mockResolvedValue("main");
71
+ });
72
+
73
+ afterEach(() => {
74
+ jest.restoreAllMocks();
75
+ });
76
+
77
+ describe("the default-branch invariant", () => {
78
+ test("commit_code_to_branch REFUSES the repository's default branch", async () => {
79
+ const commitSpy: jest.SpiedFunction<typeof GitHubUtil.commitFilesToBranch> =
80
+ jest.spyOn(GitHubUtil, "commitFilesToBranch");
81
+
82
+ await expect(
83
+ CommitCodeToBranchTool.execute(
84
+ {
85
+ branchName: "main",
86
+ commitMessage: "fix",
87
+ changes: CHANGES,
88
+ },
89
+ ctx,
90
+ ),
91
+ ).rejects.toThrow(/default branch .* may never commit to it/);
92
+
93
+ // The refusal must happen BEFORE any write is attempted.
94
+ expect(commitSpy).not.toHaveBeenCalled();
95
+ });
96
+
97
+ /*
98
+ * mainBranchName is a copy made at import time and drifts. GitHub's answer
99
+ * is the authority — otherwise a repo that renamed main -> trunk would let
100
+ * chat write straight to trunk.
101
+ */
102
+ test("refuses GitHub's real default branch even when our stored column disagrees", async () => {
103
+ jest.spyOn(GitHubUtil, "getDefaultBranchName").mockResolvedValue("trunk");
104
+ const commitSpy: jest.SpiedFunction<typeof GitHubUtil.commitFilesToBranch> =
105
+ jest.spyOn(GitHubUtil, "commitFilesToBranch");
106
+
107
+ await expect(
108
+ CommitCodeToBranchTool.execute(
109
+ { branchName: "trunk", commitMessage: "fix", changes: CHANGES },
110
+ ctx,
111
+ ),
112
+ ).rejects.toThrow(/default branch/);
113
+
114
+ expect(commitSpy).not.toHaveBeenCalled();
115
+ });
116
+
117
+ test("the default-branch refusal is case-insensitive", async () => {
118
+ await expect(
119
+ CommitCodeToBranchTool.execute(
120
+ { branchName: "MAIN", commitMessage: "fix", changes: CHANGES },
121
+ ctx,
122
+ ),
123
+ ).rejects.toThrow(/default branch/);
124
+ });
125
+
126
+ test("commit_code_to_branch REFUSES a protected branch", async () => {
127
+ jest.spyOn(GitHubUtil, "getBranch").mockResolvedValue({
128
+ name: "release",
129
+ headSha: "abc123",
130
+ isProtected: true,
131
+ });
132
+ const commitSpy: jest.SpiedFunction<typeof GitHubUtil.commitFilesToBranch> =
133
+ jest.spyOn(GitHubUtil, "commitFilesToBranch");
134
+
135
+ await expect(
136
+ CommitCodeToBranchTool.execute(
137
+ { branchName: "release", commitMessage: "fix", changes: CHANGES },
138
+ ctx,
139
+ ),
140
+ ).rejects.toThrow(/is protected/);
141
+
142
+ expect(commitSpy).not.toHaveBeenCalled();
143
+ });
144
+
145
+ test("open_code_pull_request never targets the default branch as its head", async () => {
146
+ jest.spyOn(GitHubUtil, "createBranch").mockResolvedValue(true);
147
+ jest.spyOn(GitHubUtil, "commitFilesToBranch").mockResolvedValue({
148
+ commitSha: "c1",
149
+ branchName: "x",
150
+ htmlUrl: "u",
151
+ });
152
+ jest.spyOn(AIAgentTaskPullRequestService, "countBy").mockResolvedValue({
153
+ toNumber: () => {
154
+ return 0;
155
+ },
156
+ } as never);
157
+ jest
158
+ .spyOn(AIAgentTaskPullRequestService, "create")
159
+ .mockResolvedValue({} as never);
160
+
161
+ const prSpy: jest.SpiedFunction<
162
+ typeof GitHubUtil.createPullRequestWithToken
163
+ > = jest.spyOn(GitHubUtil, "createPullRequestWithToken").mockResolvedValue({
164
+ pullRequestId: 1,
165
+ pullRequestNumber: 7,
166
+ state: PullRequestState.Open,
167
+ } as never);
168
+
169
+ await OpenCodePullRequestTool.execute(
170
+ { title: "Fix charge", description: "why", changes: CHANGES },
171
+ ctx,
172
+ );
173
+
174
+ const call: { baseBranchName: string; headBranchName: string } = prSpy.mock
175
+ .calls[0]![0] as { baseBranchName: string; headBranchName: string };
176
+
177
+ // Base is the default branch (we merge INTO it); head must never be.
178
+ expect(call.baseBranchName).toBe("main");
179
+ expect(call.headBranchName).not.toBe("main");
180
+ expect(call.headBranchName).toContain("oneuptime-ai/");
181
+ });
182
+ });
183
+
184
+ describe("open_code_pull_request", () => {
185
+ function mockHappyPath(): void {
186
+ jest.spyOn(GitHubUtil, "createBranch").mockResolvedValue(true);
187
+ jest.spyOn(GitHubUtil, "commitFilesToBranch").mockResolvedValue({
188
+ commitSha: "c1",
189
+ branchName: "x",
190
+ htmlUrl: "u",
191
+ });
192
+ jest.spyOn(AIAgentTaskPullRequestService, "countBy").mockResolvedValue({
193
+ toNumber: () => {
194
+ return 0;
195
+ },
196
+ } as never);
197
+ jest
198
+ .spyOn(AIAgentTaskPullRequestService, "create")
199
+ .mockResolvedValue({} as never);
200
+ jest.spyOn(GitHubUtil, "createPullRequestWithToken").mockResolvedValue({
201
+ pullRequestId: 1,
202
+ pullRequestNumber: 7,
203
+ state: PullRequestState.Open,
204
+ } as never);
205
+ }
206
+
207
+ test("opens a DRAFT pull request and says it is not merged", async () => {
208
+ mockHappyPath();
209
+
210
+ const result: ToolExecutionResult = await OpenCodePullRequestTool.execute(
211
+ { title: "Fix charge", description: "why", changes: CHANGES },
212
+ ctx,
213
+ );
214
+
215
+ const prCall: { isDraft: boolean } = (
216
+ GitHubUtil.createPullRequestWithToken as unknown as jest.Mock
217
+ ).mock.calls[0][0];
218
+ expect(prCall.isDraft).toBe(true);
219
+
220
+ expect(result.dataForLlm).toContain("DRAFT");
221
+ expect(result.dataForLlm).toContain("NOT merged");
222
+ expect(result.dataForLlm).toContain("/pull/7");
223
+ });
224
+
225
+ /*
226
+ * Chat PRs are recorded as AIAgentTaskPullRequest rows so the same per-repo
227
+ * cap the autonomous agent obeys can see them. If they were not recorded,
228
+ * chat would be an uncapped way to flood the review queue the cap protects.
229
+ */
230
+ test("records the PR so it counts against the open-PR cap", async () => {
231
+ mockHappyPath();
232
+
233
+ await OpenCodePullRequestTool.execute(
234
+ { title: "Fix charge", description: "why", changes: CHANGES },
235
+ ctx,
236
+ );
237
+
238
+ const created: {
239
+ data: { codeRepositoryId: ObjectID; headRefName: string };
240
+ } = (AIAgentTaskPullRequestService.create as unknown as jest.Mock).mock
241
+ .calls[0][0];
242
+
243
+ expect(created.data.codeRepositoryId.toString()).toBe(REPO_ID.toString());
244
+ expect(created.data.headRefName).toContain("oneuptime-ai/");
245
+ });
246
+
247
+ /*
248
+ * The record MUST satisfy the model's real required-column validation.
249
+ *
250
+ * This test exists because mocking AIAgentTaskPullRequestService.create — as
251
+ * every other test here does, to stay off a database — bypasses
252
+ * checkRequiredFields entirely. That hid a bug where aiAgentId was NOT NULL
253
+ * and never set, so create() threw on every call *after* the pull request was
254
+ * already open on GitHub: the user got an error, the row was never written,
255
+ * the open-PR cap counted 0 chat PRs forever, and the model retried and
256
+ * opened duplicate PRs. Nineteen green tests said otherwise.
257
+ *
258
+ * So: capture what the tool really builds, and run the REAL validator on it.
259
+ */
260
+ test("the recorded PR row passes the model's real required-field validation", async () => {
261
+ mockHappyPath();
262
+
263
+ await OpenCodePullRequestTool.execute(
264
+ { title: "Fix charge", description: "why", changes: CHANGES },
265
+ ctx,
266
+ );
267
+
268
+ const record: AIAgentTaskPullRequest = (
269
+ AIAgentTaskPullRequestService.create as unknown as jest.Mock
270
+ ).mock.calls[0][0].data;
271
+
272
+ // Not a re-implementation of the rule — the actual production validator.
273
+ const validate: (model: AIAgentTaskPullRequest) => AIAgentTaskPullRequest =
274
+ (
275
+ AIAgentTaskPullRequestService as unknown as {
276
+ checkRequiredFields: (
277
+ model: AIAgentTaskPullRequest,
278
+ ) => AIAgentTaskPullRequest;
279
+ }
280
+ ).checkRequiredFields.bind(AIAgentTaskPullRequestService);
281
+
282
+ expect(() => {
283
+ return validate(record);
284
+ }).not.toThrow();
285
+ });
286
+
287
+ test("refuses when the repository is at its open-PR cap", async () => {
288
+ mockHappyPath();
289
+ jest.spyOn(AIAgentTaskPullRequestService, "countBy").mockResolvedValue({
290
+ toNumber: () => {
291
+ return 5;
292
+ },
293
+ } as never);
294
+
295
+ const branchSpy: jest.SpiedFunction<typeof GitHubUtil.createBranch> =
296
+ jest.spyOn(GitHubUtil, "createBranch");
297
+
298
+ await expect(
299
+ OpenCodePullRequestTool.execute(
300
+ { title: "Fix charge", description: "why", changes: CHANGES },
301
+ ctx,
302
+ ),
303
+ ).rejects.toThrow(/open AI fix pull request cap/);
304
+
305
+ // Rejected before creating a branch — no orphan branches from a capped repo.
306
+ expect(branchSpy).not.toHaveBeenCalled();
307
+ });
308
+ });
309
+
310
+ describe("change validation (all of this is untrusted LLM output)", () => {
311
+ test("rejects a path traversal", async () => {
312
+ await expect(
313
+ OpenCodePullRequestTool.execute(
314
+ {
315
+ title: "t",
316
+ description: "d",
317
+ changes: [{ filePath: "../../etc/passwd", content: "x" }],
318
+ },
319
+ ctx,
320
+ ),
321
+ ).rejects.toThrow(/not a valid repository-relative path/);
322
+ });
323
+
324
+ test("rejects the same file written twice in one commit", async () => {
325
+ await expect(
326
+ OpenCodePullRequestTool.execute(
327
+ {
328
+ title: "t",
329
+ description: "d",
330
+ changes: [
331
+ { filePath: "a.ts", content: "1" },
332
+ { filePath: "a.ts", content: "2" },
333
+ ],
334
+ },
335
+ ctx,
336
+ ),
337
+ ).rejects.toThrow(/appears twice/);
338
+ });
339
+
340
+ test("rejects more than the per-commit file cap", async () => {
341
+ const many: Array<{ filePath: string; content: string }> = Array.from(
342
+ { length: 11 },
343
+ (_v: unknown, i: number) => {
344
+ return { filePath: `f${i}.ts`, content: "x" };
345
+ },
346
+ );
347
+
348
+ await expect(
349
+ OpenCodePullRequestTool.execute(
350
+ { title: "t", description: "d", changes: many },
351
+ ctx,
352
+ ),
353
+ ).rejects.toThrow(/at most 10 files/);
354
+ });
355
+
356
+ test("rejects an empty changes array", async () => {
357
+ await expect(
358
+ OpenCodePullRequestTool.execute(
359
+ { title: "t", description: "d", changes: [] },
360
+ ctx,
361
+ ),
362
+ ).rejects.toThrow(/changes is required/);
363
+ });
364
+
365
+ test("rejects a change whose content is not a string", async () => {
366
+ await expect(
367
+ OpenCodePullRequestTool.execute(
368
+ {
369
+ title: "t",
370
+ description: "d",
371
+ changes: [{ filePath: "a.ts", content: 42 }],
372
+ },
373
+ ctx,
374
+ ),
375
+ ).rejects.toThrow(/needs a content string/);
376
+ });
377
+
378
+ // An empty file body is legitimate; getString would wrongly reject it.
379
+ test("allows an empty string as a new file's contents", async () => {
380
+ jest.spyOn(GitHubUtil, "getBranch").mockResolvedValue({
381
+ name: "feature",
382
+ headSha: "abc",
383
+ isProtected: false,
384
+ });
385
+ const commitSpy: jest.SpiedFunction<typeof GitHubUtil.commitFilesToBranch> =
386
+ jest.spyOn(GitHubUtil, "commitFilesToBranch").mockResolvedValue({
387
+ commitSha: "c1",
388
+ branchName: "feature",
389
+ htmlUrl: "u",
390
+ });
391
+
392
+ await CommitCodeToBranchTool.execute(
393
+ {
394
+ branchName: "feature",
395
+ commitMessage: "add placeholder",
396
+ changes: [{ filePath: "a.ts", content: "" }],
397
+ },
398
+ ctx,
399
+ );
400
+
401
+ expect(commitSpy).toHaveBeenCalled();
402
+ });
403
+
404
+ test("strips a leading slash rather than failing", async () => {
405
+ jest.spyOn(GitHubUtil, "getBranch").mockResolvedValue({
406
+ name: "feature",
407
+ headSha: "abc",
408
+ isProtected: false,
409
+ });
410
+ const commitSpy: jest.SpiedFunction<typeof GitHubUtil.commitFilesToBranch> =
411
+ jest.spyOn(GitHubUtil, "commitFilesToBranch").mockResolvedValue({
412
+ commitSha: "c1",
413
+ branchName: "feature",
414
+ htmlUrl: "u",
415
+ });
416
+
417
+ await CommitCodeToBranchTool.execute(
418
+ {
419
+ branchName: "feature",
420
+ commitMessage: "m",
421
+ changes: [{ filePath: "/src/a.ts", content: "x" }],
422
+ },
423
+ ctx,
424
+ );
425
+
426
+ const call: { changes: Array<{ filePath: string }> } = commitSpy.mock
427
+ .calls[0]![0] as { changes: Array<{ filePath: string }> };
428
+ expect(call.changes[0]!.filePath).toBe("src/a.ts");
429
+ });
430
+ });
431
+
432
+ describe("permission-mode gating", () => {
433
+ /*
434
+ * Both tools must be classified as mutations, or ReadOnly conversations
435
+ * would be offered code-writing tools and AskForApproval would run them
436
+ * with no approval card.
437
+ */
438
+ test("both code-write tools are registered as mutations", () => {
439
+ expect(AIToolbox.isMutationTool("open_code_pull_request")).toBe(true);
440
+ expect(AIToolbox.isMutationTool("commit_code_to_branch")).toBe(true);
441
+ });
442
+
443
+ test("the tool registry has no undefined entries", () => {
444
+ /*
445
+ * A cycle that loads a tool module before Index leaves undefined holes in
446
+ * the tools array, which breaks getToolByName for EVERY tool, not just the
447
+ * new ones. Cheap guard against a whole-toolbox outage.
448
+ */
449
+ const tools: Array<unknown> = AIToolbox.getTools();
450
+ expect(tools.length).toBeGreaterThan(0);
451
+ expect(
452
+ tools.filter((tool: unknown) => {
453
+ return !tool;
454
+ }),
455
+ ).toEqual([]);
456
+ });
457
+
458
+ test("read-only code tools are NOT mutations", () => {
459
+ expect(AIToolbox.isMutationTool("read_code_file")).toBe(false);
460
+ expect(AIToolbox.isMutationTool("find_code_for_exception")).toBe(false);
461
+ });
462
+
463
+ test("the approval card names the action concretely", () => {
464
+ expect(
465
+ OpenCodePullRequestTool.buildActionTitle!({ title: "Fix charge bug" }),
466
+ ).toBe("Open draft pull request: Fix charge bug");
467
+
468
+ expect(
469
+ CommitCodeToBranchTool.buildActionTitle!({
470
+ branchName: "feature/x",
471
+ changes: [{ filePath: "a.ts", content: "" }],
472
+ }),
473
+ ).toBe("Commit 1 file to feature/x");
474
+ });
475
+ });