@oneuptime/common 13.0.0 → 13.0.1

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 (734) hide show
  1. package/Models/DatabaseModels/Alert.ts +55 -0
  2. package/Models/DatabaseModels/CloudResource.ts +45 -7
  3. package/Models/DatabaseModels/CodeRepository.ts +85 -0
  4. package/Models/DatabaseModels/GoogleSecOpsConnection.ts +57 -7
  5. package/Models/DatabaseModels/GoogleSecOpsConnectionRun.ts +263 -0
  6. package/Models/DatabaseModels/Incident.ts +55 -0
  7. package/Models/DatabaseModels/Index.ts +16 -0
  8. package/Models/DatabaseModels/ScheduledMaintenance.ts +55 -0
  9. package/Models/DatabaseModels/User.ts +51 -2
  10. package/Models/DatabaseModels/UserNotificationEmailRollupItem.ts +43 -2
  11. package/Models/DatabaseModels/UserWebAuthn.ts +17 -0
  12. package/Models/DatabaseModels/VMwareResource.ts +1149 -0
  13. package/Models/DatabaseModels/VMwareVCenter.ts +1122 -0
  14. package/Models/DatabaseModels/VMwareVCenterFeed.ts +625 -0
  15. package/Models/DatabaseModels/VMwareVCenterLabelRule.ts +514 -0
  16. package/Models/DatabaseModels/VMwareVCenterOwnerRule.ts +596 -0
  17. package/Models/DatabaseModels/VMwareVCenterOwnerTeam.ts +485 -0
  18. package/Models/DatabaseModels/VMwareVCenterOwnerUser.ts +484 -0
  19. package/Models/DatabaseModels/WorkflowVariable.ts +22 -1
  20. package/Server/API/AIAgentDataAPI.ts +79 -4
  21. package/Server/API/AIAgentGitHubAPI.ts +523 -0
  22. package/Server/API/AIChatAPI.ts +20 -1
  23. package/Server/API/BillingAPI.ts +33 -0
  24. package/Server/API/DashboardAPI.ts +10 -0
  25. package/Server/API/GitHubAPI.ts +47 -5
  26. package/Server/API/GoogleSecOpsConnectionAPI.ts +79 -0
  27. package/Server/API/UserWebAuthnAPI.ts +21 -1
  28. package/Server/API/VMwareResourceAPI.ts +143 -0
  29. package/Server/Infrastructure/Postgres/SchemaMigrations/1791800000000-SplitWebAuthnChallengeByPurpose.ts +68 -0
  30. package/Server/Infrastructure/Postgres/SchemaMigrations/1791900000000-AddSeverityAndStateToNotificationEmailRollup.ts +26 -0
  31. package/Server/Infrastructure/Postgres/SchemaMigrations/1792000000000-AddVMwareTables.ts +636 -0
  32. package/Server/Infrastructure/Postgres/SchemaMigrations/1792100000000-AddGitHubCommandSettingsToCodeRepository.ts +47 -0
  33. package/Server/Infrastructure/Postgres/SchemaMigrations/1792200000000-AddUserWebAuthnPurpose.ts +17 -0
  34. package/Server/Infrastructure/Postgres/SchemaMigrations/1792300000000-AddGoogleSecOpsDiagnostics.ts +83 -0
  35. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +12 -0
  36. package/Server/Middleware/IdentityRateLimit.ts +101 -0
  37. package/Server/Services/AIRunService.ts +13 -0
  38. package/Server/Services/BillingPaymentMethodService.ts +99 -28
  39. package/Server/Services/BillingService.ts +287 -20
  40. package/Server/Services/CloudResourceInstanceService.ts +70 -0
  41. package/Server/Services/ExceptionAggregationService.ts +1 -0
  42. package/Server/Services/GoogleSecOpsConnectionRunService.ts +10 -0
  43. package/Server/Services/GoogleSecOpsConnectionService.ts +12 -0
  44. package/Server/Services/Index.ts +16 -0
  45. package/Server/Services/LogAggregationService.ts +1 -0
  46. package/Server/Services/MetricAggregationService.ts +1 -0
  47. package/Server/Services/MobilePasskeyLoginService.ts +216 -0
  48. package/Server/Services/MonitorService.ts +51 -1
  49. package/Server/Services/MonitorTemplateService.ts +91 -29
  50. package/Server/Services/NetworkDeviceAutoImportRuleEngineService.ts +132 -1
  51. package/Server/Services/NetworkDeviceAutoImportRuleService.ts +176 -2
  52. package/Server/Services/OpenTelemetryIngestService.ts +17 -0
  53. package/Server/Services/PayAsYouGoBillingService.ts +446 -0
  54. package/Server/Services/ProjectService.ts +46 -2
  55. package/Server/Services/ServerlessFunctionInstanceService.ts +71 -0
  56. package/Server/Services/TeamMemberService.ts +96 -36
  57. package/Server/Services/TelemetryIngestionKeyService.ts +80 -10
  58. package/Server/Services/TelemetryUsageBillingService.ts +95 -5
  59. package/Server/Services/TraceAggregationService.ts +5 -4
  60. package/Server/Services/UserWebAuthnService.ts +592 -133
  61. package/Server/Services/VMwareResourceService.ts +648 -0
  62. package/Server/Services/VMwareVCenterFeedService.ts +99 -0
  63. package/Server/Services/VMwareVCenterLabelRuleEngineService.ts +240 -0
  64. package/Server/Services/VMwareVCenterLabelRuleService.ts +14 -0
  65. package/Server/Services/VMwareVCenterOwnerRuleEngineService.ts +252 -0
  66. package/Server/Services/VMwareVCenterOwnerRuleService.ts +14 -0
  67. package/Server/Services/VMwareVCenterOwnerTeamService.ts +139 -0
  68. package/Server/Services/VMwareVCenterOwnerUserService.ts +133 -0
  69. package/Server/Services/VMwareVCenterService.ts +629 -0
  70. package/Server/Services/WorkflowVariableService.ts +203 -0
  71. package/Server/Types/Billing/MeteredPlan/ActiveMonitoringMeteredPlan.ts +17 -0
  72. package/Server/Types/Billing/MeteredPlan/TelemetryMeteredPlan.ts +31 -1
  73. package/Server/Utils/CodeRepository/GitHub/GitHub.ts +7 -0
  74. package/Server/Utils/CodeRepository/GitHub/GitHubAgentTaskTrigger.ts +341 -0
  75. package/Server/Utils/CodeRepository/GitHub/GitHubCommandAuthorizer.ts +274 -0
  76. package/Server/Utils/CodeRepository/GitHub/GitHubCommandParser.ts +375 -0
  77. package/Server/Utils/CodeRepository/GitHub/GitHubConversation.ts +846 -0
  78. package/Server/Utils/CodeRepository/GitHub/GitHubReplyComposer.ts +360 -0
  79. package/Server/Utils/CodeRepository/GitHub/GitHubRunReply.ts +421 -0
  80. package/Server/Utils/CodeRepository/GitHub/GitHubWebhookEvents.ts +140 -0
  81. package/Server/Utils/CodeRepository/GitHub/GitHubWebhookHandler.ts +906 -0
  82. package/Server/Utils/Dashboard/PublicDashboardResourceListPolicy.ts +143 -0
  83. package/Server/Utils/DataSource/EgressGuard.ts +160 -15
  84. package/Server/Utils/Database/TruncateColumnValue.ts +1 -1
  85. package/Server/Utils/EmailRollup/EmailRollupFlushRunner.ts +2 -0
  86. package/Server/Utils/EmailRollup/EmailRollupRenderer.ts +33 -0
  87. package/Server/Utils/EmailRollup/EmailRollupWriter.ts +44 -0
  88. package/Server/Utils/LogRedaction.ts +2 -1
  89. package/Server/Utils/Monitor/MonitorAlert.ts +1 -1
  90. package/Server/Utils/Monitor/MonitorCriteriaEvaluator.ts +302 -3
  91. package/Server/Utils/Monitor/MonitorMaintenanceSuppression.ts +26 -4
  92. package/Server/Utils/Monitor/MonitorResourceContext.ts +5 -3
  93. package/Server/Utils/Monitor/MonitorStepResourceIdentity.ts +14 -0
  94. package/Server/Utils/Monitor/MonitorTemplateUtil.ts +1 -0
  95. package/Server/Utils/Monitor/SeriesResourceLabels.ts +26 -0
  96. package/Server/Utils/Monitor/SeriesResourceLinker.ts +28 -5
  97. package/Server/Utils/ResourceFeed/ResourceFeedUtil.ts +3 -3
  98. package/Server/Utils/SecurityEvent/ConnectorErrorMessage.ts +14 -9
  99. package/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsClient.ts +116 -84
  100. package/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsPoller.ts +594 -195
  101. package/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsRunExecutor.ts +466 -0
  102. package/Server/Utils/Telemetry/EntityRegistry.ts +5 -0
  103. package/Server/Utils/Telemetry/ResourceHeartbeat.ts +2 -1
  104. package/Server/Utils/Telemetry/TelemetryEntity.ts +181 -1
  105. package/Server/Utils/Telemetry/VMwareSnapshotScan.ts +1352 -0
  106. package/Tests/App/Accounts/LoginPasskey.test.tsx +529 -0
  107. package/Tests/App/Accounts/MobilePasskey.test.tsx +620 -0
  108. package/Tests/App/Accounts/PasswordRequirements.test.tsx +254 -0
  109. package/Tests/App/Accounts/RegisterPassword.test.tsx +553 -0
  110. package/Tests/App/Dashboard/AskAiAvailability.test.ts +224 -0
  111. package/Tests/App/Dashboard/AskAiDisabled.test.tsx +432 -0
  112. package/Tests/App/Dashboard/BackupCodes.test.tsx +414 -0
  113. package/Tests/App/Dashboard/BillingPaidUsageFlow.test.tsx +530 -0
  114. package/Tests/App/Dashboard/BillingPaymentMethodForm.test.tsx +195 -0
  115. package/Tests/App/Dashboard/BreadcrumbCoverage.test.tsx +199 -0
  116. package/Tests/App/Dashboard/CloudDocumentationCard.test.tsx +198 -0
  117. package/Tests/App/Dashboard/CloudDocumentationMarkdown.test.ts +215 -0
  118. package/Tests/App/Dashboard/CloudFleetSummary.test.ts +183 -0
  119. package/Tests/App/Dashboard/CloudFleetSummaryComponent.test.tsx +248 -0
  120. package/Tests/App/Dashboard/CloudResourceTelemetryScope.test.ts +215 -0
  121. package/Tests/App/Dashboard/CriteriaFilterDefaults.test.ts +3 -0
  122. package/Tests/App/Dashboard/DashboardHeaderRequests.test.tsx +441 -0
  123. package/Tests/App/Dashboard/DashboardLazyPages.test.tsx +458 -0
  124. package/Tests/App/Dashboard/DashboardToolbarPopupLayering.test.tsx +566 -0
  125. package/Tests/App/Dashboard/DiscoveryScanLivePage.test.tsx +376 -0
  126. package/Tests/App/Dashboard/DiscoveryScanLiveUpdates.test.tsx +642 -0
  127. package/Tests/App/Dashboard/DiscoveryScanProgress.test.tsx +267 -0
  128. package/Tests/App/Dashboard/DiscoveryScanWizardValidation.test.tsx +69 -231
  129. package/Tests/App/Dashboard/GoogleSecOpsConnectionsErrors.test.tsx +244 -0
  130. package/Tests/App/Dashboard/GoogleSecOpsDiagnostics.test.tsx +724 -0
  131. package/Tests/App/Dashboard/MonitorTemplateSyncFields.test.tsx +539 -0
  132. package/Tests/App/Dashboard/OnCallLayerCardOverrides.test.tsx +24 -7
  133. package/Tests/App/Dashboard/OnCallOverridePresentation.test.ts +496 -0
  134. package/Tests/App/Dashboard/OnCallScheduleOverrideClarity.test.tsx +677 -0
  135. package/Tests/App/Dashboard/OnCallSchedulePreviewOverrides.test.tsx +31 -3
  136. package/Tests/App/Dashboard/PaidUsageConsent.test.tsx +180 -0
  137. package/Tests/App/Dashboard/PasskeySettings.test.tsx +1220 -0
  138. package/Tests/App/Dashboard/PayAsYouGoConsentGate.test.tsx +176 -275
  139. package/Tests/App/Dashboard/PayAsYouGoNotices.test.tsx +380 -233
  140. package/Tests/App/Dashboard/ProductMenuBreadcrumbs.test.tsx +3 -23
  141. package/Tests/App/Dashboard/ResourceFeed.test.tsx +1 -0
  142. package/Tests/App/Dashboard/ResourceFeedPageWiring.test.ts +8 -1
  143. package/Tests/App/Dashboard/TwoFactorStatus.test.tsx +260 -0
  144. package/Tests/App/Dashboard/VMwareDashboardWidgets.test.ts +573 -0
  145. package/Tests/App/Dashboard/WidgetCatalog.test.ts +2 -0
  146. package/Tests/App/Dashboard/WorkflowVariablesTable.test.tsx +768 -0
  147. package/Tests/App/StatusPage/StatusPageOverviewLiveAndSearch.test.tsx +532 -40
  148. package/Tests/App/StatusPage/StatusPageResourceSearchBox.test.tsx +19 -2
  149. package/Tests/Fixtures/MobilePasskeyUser.json +8 -0
  150. package/Tests/Models/DatabaseModels/ResourceFeedModels.test.ts +25 -6
  151. package/Tests/Models/DatabaseModels/VMwareModels.test.ts +1134 -0
  152. package/Tests/Models/RunnerVersionLabel.test.ts +4 -2
  153. package/Tests/Server/API/AIChatProvidersAIEnabled.test.ts +259 -0
  154. package/Tests/Server/API/AIChatRouteAuthorization.test.ts +31 -2
  155. package/Tests/Server/API/BillingPayAsYouGoStatus.test.ts +116 -0
  156. package/Tests/Server/API/DashboardPublicResourceListAPI.test.ts +29 -1
  157. package/Tests/Server/API/EnterpriseLicenseReportUserCount.test.ts +13 -2
  158. package/Tests/Server/API/GitHubWebhookSignature.test.ts +368 -0
  159. package/Tests/Server/API/GoogleSecOpsConnectionAPI.test.ts +248 -0
  160. package/Tests/Server/API/UserWebAuthnAuthenticationOptionsRoute.test.ts +330 -0
  161. package/Tests/Server/API/UserWebAuthnPasskeyAPI.test.ts +172 -0
  162. package/Tests/Server/API/VMwareResourceAPI.test.ts +546 -0
  163. package/Tests/Server/Infrastructure/Postgres/UserNotificationEmailRollupMetadataMigration.test.ts +85 -0
  164. package/Tests/Server/Infrastructure/Postgres/UserNotificationEmailRollupTableMigration.test.ts +34 -3
  165. package/Tests/Server/Middleware/PasskeyRateLimit.test.ts +141 -0
  166. package/Tests/Server/Services/AutoImportRuleRearmRecentScans.test.ts +343 -0
  167. package/Tests/Server/Services/AutoImportRuleWriteRearmsScans.test.ts +381 -0
  168. package/Tests/Server/Services/BillingPaymentMethodServiceSync.test.ts +175 -0
  169. package/Tests/Server/Services/BillingService.test.ts +26 -11
  170. package/Tests/Server/Services/BillingServiceMeteredPaymentAuthorization.test.ts +245 -0
  171. package/Tests/Server/Services/BillingServicePaymentMethodReads.test.ts +362 -0
  172. package/Tests/Server/Services/CloudResourceInstanceStalePrune.test.ts +188 -0
  173. package/Tests/Server/Services/DiscoveryScanClaimHookFreeSafety.test.ts +16 -2
  174. package/Tests/Server/Services/GoogleSecOpsDiagnosticsModel.test.ts +153 -0
  175. package/Tests/Server/Services/HookFreeWriteLengthClamp.test.ts +13 -0
  176. package/Tests/Server/Services/InventoryItemPromotionGate.test.ts +5 -0
  177. package/Tests/Server/Services/InventoryUpsertSchemaParity.test.ts +36 -0
  178. package/Tests/Server/Services/InventoryUpsertTruncationGuard.test.ts +151 -0
  179. package/Tests/Server/Services/MobilePasskeyLoginIntegration.test.ts +231 -0
  180. package/Tests/Server/Services/MobilePasskeyLoginService.test.ts +315 -0
  181. package/Tests/Server/Services/MonitorPayAsYouGoBilling.test.ts +302 -0
  182. package/Tests/Server/Services/MonitorTemplateServiceFieldSync.test.ts +994 -0
  183. package/Tests/Server/Services/NetworkDeviceAutoImportRuleService.test.ts +22 -0
  184. package/Tests/Server/Services/OpenTelemetryResourceRetentionMemo.test.ts +7 -1
  185. package/Tests/Server/Services/PayAsYouGoBillingService.test.ts +857 -0
  186. package/Tests/Server/Services/PayAsYouGoStripeRateLimitBudget.test.ts +379 -0
  187. package/Tests/Server/Services/ProjectServiceExtendTrial.test.ts +99 -1
  188. package/Tests/Server/Services/ProjectServiceSubscriptionSeats.test.ts +134 -0
  189. package/Tests/Server/Services/ResourceFeedServices.test.ts +3 -2
  190. package/Tests/Server/Services/ResourceUpdateLastSeenLivenessFallback.test.ts +32 -6
  191. package/Tests/Server/Services/SecurityEventLastErrorColumnWidth.test.ts +363 -175
  192. package/Tests/Server/Services/ServerlessFunctionInstanceStalePrune.test.ts +200 -0
  193. package/Tests/Server/Services/TeamMemberAutoAcceptInvitation.test.ts +3 -0
  194. package/Tests/Server/Services/TeamMemberBillingSeatSync.test.ts +762 -0
  195. package/Tests/Server/Services/TeamMemberInviteRegistrationToken.test.ts +3 -0
  196. package/Tests/Server/Services/TelemetryIngestionKeyPolicyResolution.test.ts +72 -0
  197. package/Tests/Server/Services/TelemetryIngestionKeyValidation.test.ts +17 -4
  198. package/Tests/Server/Services/TelemetryPayAsYouGoBilling.test.ts +355 -0
  199. package/Tests/Server/Services/TraceAnalyticsResourceDisplayNames.test.ts +41 -4
  200. package/Tests/Server/Services/UserNotificationEmailRollupModels.test.ts +37 -0
  201. package/Tests/Server/Services/UserWebAuthnPasskeyIntegration.test.ts +401 -0
  202. package/Tests/Server/Services/UserWebAuthnPasskeyService.test.ts +769 -0
  203. package/Tests/Server/Services/UserWebAuthnUserVerification.test.ts +649 -0
  204. package/Tests/Server/Services/VMwareResourceService.test.ts +945 -0
  205. package/Tests/Server/Services/VMwareVCenterService.test.ts +722 -0
  206. package/Tests/Server/Services/WorkflowVariableRenameUniqueness.test.ts +842 -0
  207. package/Tests/Server/TestingUtils/PasskeyHttpIntegration.ts +102 -0
  208. package/Tests/Server/TestingUtils/SecurityKey.ts +464 -0
  209. package/Tests/Server/TestingUtils/WebAuthnServiceMocks.ts +60 -0
  210. package/Tests/Server/Types/Billing/PayAsYouGoMeteredAuthorization.test.ts +573 -0
  211. package/Tests/Server/Types/Database/Permissions/ProjectCustomerSupportAccessColumn.test.ts +261 -0
  212. package/Tests/Server/Types/Database/Permissions/WorkflowVariableColumnPermission.test.ts +95 -0
  213. package/Tests/Server/Utils/CodeRepository/GitHubAgentTaskTrigger.test.ts +2101 -0
  214. package/Tests/Server/Utils/CodeRepository/GitHubCommandAuthorizer.test.ts +883 -0
  215. package/Tests/Server/Utils/CodeRepository/GitHubCommandParser.test.ts +1474 -0
  216. package/Tests/Server/Utils/CodeRepository/GitHubConversation.test.ts +1526 -0
  217. package/Tests/Server/Utils/CodeRepository/GitHubReplyComposer.test.ts +1389 -0
  218. package/Tests/Server/Utils/CodeRepository/GitHubRunReply.test.ts +1707 -0
  219. package/Tests/Server/Utils/CodeRepository/GitHubWebhookHandler.test.ts +2202 -0
  220. package/Tests/Server/Utils/Dashboard/PublicDashboardResourceListPolicy.test.ts +345 -0
  221. package/Tests/Server/Utils/DataSource/EgressGuard.test.ts +896 -0
  222. package/Tests/Server/Utils/EmailRollup/EmailRollupMetadataIntegration.test.ts +246 -0
  223. package/Tests/Server/Utils/EmailRollup/EmailRollupRenderer.test.ts +299 -1
  224. package/Tests/Server/Utils/EmailRollup/EmailRollupTestHarness.ts +6 -0
  225. package/Tests/Server/Utils/EmailRollup/EmailRollupWriter.test.ts +185 -0
  226. package/Tests/Server/Utils/LogRedaction.test.ts +33 -0
  227. package/Tests/Server/Utils/Monitor/CapturedMetricAttributeUtil.test.ts +2 -1
  228. package/Tests/Server/Utils/Monitor/Criteria/TelemetryStepMetricAlias.test.ts +10 -0
  229. package/Tests/Server/Utils/Monitor/MonitorAlertResourceLinking.test.ts +66 -1
  230. package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluator.test.ts +362 -0
  231. package/Tests/Server/Utils/Monitor/MonitorDependencySuppressionCreatorSkip.test.ts +1 -0
  232. package/Tests/Server/Utils/Monitor/MonitorIncidentResourceLinking.test.ts +60 -1
  233. package/Tests/Server/Utils/Monitor/MonitorMaintenanceSuppression.test.ts +60 -0
  234. package/Tests/Server/Utils/Monitor/MonitorRootCauseTargetResolution.test.ts +423 -0
  235. package/Tests/Server/Utils/Monitor/MonitorStepResourceIdentity.test.ts +6 -0
  236. package/Tests/Server/Utils/Monitor/MonitorSummaryPersistence.test.ts +1 -0
  237. package/Tests/Server/Utils/Monitor/PerSeriesSecondEntityAlerting.test.ts +1 -0
  238. package/Tests/Server/Utils/Monitor/SeriesResourceLinker.test.ts +46 -2
  239. package/Tests/Server/Utils/SecurityEvent/ConnectorErrorMessage.test.ts +81 -45
  240. package/Tests/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsRunExecutor.test.ts +711 -0
  241. package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsClickhouseIntegration.test.ts +223 -0
  242. package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsClient.test.ts +69 -1
  243. package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsDiagnostics.test.ts +838 -0
  244. package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsErrorHandling.test.ts +241 -28
  245. package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsHttpIntegration.test.ts +651 -0
  246. package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsPoller.test.ts +11 -0
  247. package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsPollerFailureTaxonomy.test.ts +13 -12
  248. package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsPollerHardening.test.ts +112 -127
  249. package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsRequestContract.test.ts +12 -18
  250. package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsResponseParsing.test.ts +43 -0
  251. package/Tests/Server/Utils/Telemetry/TelemetryEntity.test.ts +580 -0
  252. package/Tests/Server/Utils/Telemetry/VMwareSnapshotScan.test.ts +1748 -0
  253. package/Tests/Types/Cloud/CloudPlatform.test.ts +574 -0
  254. package/Tests/Types/Monitor/MetricAndProfileDefaultCriteria.test.ts +2 -0
  255. package/Tests/Types/Monitor/MonitorCriteriaMetricVariables.test.ts +9 -0
  256. package/Tests/Types/Monitor/MonitorStepDefaultTelemetryConfig.test.ts +1 -0
  257. package/Tests/Types/Monitor/MonitorStepInfraMonitorUtils.test.ts +53 -8
  258. package/Tests/Types/Monitor/MonitorStepInfrastructureMonitors.test.ts +97 -0
  259. package/Tests/Types/Monitor/MonitorStepMetricViewConfigUtil.test.ts +1 -0
  260. package/Tests/Types/Monitor/MonitorStepTemplateSyncFields.test.ts +107 -0
  261. package/Tests/Types/Monitor/MonitorTemplateSyncField.test.ts +172 -0
  262. package/Tests/Types/Monitor/MonitorType.test.ts +1 -0
  263. package/Tests/Types/Monitor/MonitorTypeKeywords.test.ts +2 -0
  264. package/Tests/Types/Monitor/Recommendation/MonitorRecommendationAlertDebuggability.test.ts +1 -0
  265. package/Tests/Types/Monitor/Recommendation/MonitorRecommendationCatalog.test.ts +9 -0
  266. package/Tests/Types/Monitor/Recommendation/MonitorRecommendationCoverage.test.ts +1 -1
  267. package/Tests/Types/Monitor/Recommendation/MonitorRecommendationNotificationMode.test.ts +1 -1
  268. package/Tests/Types/Monitor/SeriesContext/SeriesDebugHints.test.ts +131 -0
  269. package/Tests/Types/Monitor/SeriesContext/SeriesLabelDisplay.test.ts +10 -0
  270. package/Tests/Types/Monitor/VMwareAlertTemplates.test.ts +1608 -0
  271. package/Tests/Types/Monitor/VMwareMetricCatalog.test.ts +488 -0
  272. package/Tests/Types/OnCallDutyPolicy/OverrideProvenance.test.ts +295 -0
  273. package/Tests/Types/Runner/RunnerLiveStatus.test.ts +2 -2
  274. package/Tests/Types/SignupPassword.test.ts +205 -0
  275. package/Tests/Types/StatusPage/StatusPageLanguage.test.ts +44 -0
  276. package/Tests/Types/SupportedLanguages.test.ts +119 -0
  277. package/Tests/Types/Telemetry/EntityTypeGroups.test.ts +19 -0
  278. package/Tests/UI/ColorPickerBuild.test.ts +86 -0
  279. package/Tests/UI/Components/Breadcrumbs.test.tsx +32 -0
  280. package/Tests/UI/Components/Forms/ModelFormPreservesUserInput.test.tsx +937 -0
  281. package/Tests/UI/Components/Forms/ModelFormWriteOnlyColumnSelect.test.tsx +230 -0
  282. package/Tests/UI/Components/Forms/ValidationFormSteps.test.ts +3 -4
  283. package/Tests/UI/Components/LabelRuleImportExport.test.tsx +907 -0
  284. package/Tests/UI/Components/ModelTable/ModelTableCreateAfterEdit.test.tsx +514 -0
  285. package/Tests/UI/Components/MonitorGraphs/Uptime.test.tsx +301 -0
  286. package/Tests/UI/Components/MonitorTemplateVariables/TemplateVariablesCatalog.test.ts +2 -1
  287. package/Tests/UI/Components/TooltipLazyContent.test.tsx +373 -0
  288. package/Tests/UI/MicrosoftTeams/MicrosoftTeamsMessagingEndpointGuidance.test.tsx +43 -12
  289. package/Tests/UI/ReactDomBuild.test.ts +104 -0
  290. package/Tests/UI/ReactRouterSingletonBuild.test.ts +11 -1
  291. package/Tests/UI/Utils/Breadcrumb/fixtures/RealBreadcrumbTrails.ts +148 -0
  292. package/Tests/UI/Utils/Breadcrumb/fixtures/RealRoutePatterns.ts +29 -0
  293. package/Tests/UI/Utils/DashboardStackingLayers.test.ts +197 -0
  294. package/Tests/UI/Utils/LabelRuleImportExport.test.ts +546 -0
  295. package/Tests/UI/Utils/ModelAPITenantHeader.test.ts +109 -2
  296. package/Tests/UI/Utils/UseDashboardGridDnd.test.tsx +7 -2
  297. package/Tests/UI/Utils/UserEmailRegistrationStatus.test.tsx +295 -0
  298. package/Tests/UI/Utils/WebAuthn.test.ts +265 -0
  299. package/Tests/Utils/Dashboard/Components/DashboardComponentDefaults.test.ts +5 -0
  300. package/Tests/Utils/Dashboard/Components/DashboardVMwareListComponents.test.ts +548 -0
  301. package/Tests/Utils/Dashboard/DashboardResourceListSharedArgs.test.ts +17 -3
  302. package/Tests/Utils/LabelRuleImportExport.test.ts +583 -0
  303. package/Tests/Utils/Monitor/MonitorSummarySnapshotUtil.test.ts +1 -0
  304. package/Tests/Utils/Monitor/MonitorTemplateSyncUtil.test.ts +590 -0
  305. package/Tests/Utils/RequestFailedDetails.test.ts +325 -0
  306. package/Tests/Utils/Telemetry/CloudInstanceIdentity.test.ts +255 -0
  307. package/Tests/Utils/Telemetry/EntityRelationship.test.ts +167 -0
  308. package/Tests/Utils/WebAuthnTestUtil.ts +79 -0
  309. package/Tests/__mocks__/simplewebauthn.js +33 -4
  310. package/Types/AI/CodeFixTaskContext.ts +122 -0
  311. package/Types/AI/CodeFixTaskType.ts +62 -0
  312. package/Types/Accounts/AccountsLanguage.ts +1 -0
  313. package/Types/AdminDashboard/AdminDashboardLanguage.ts +1 -0
  314. package/Types/Calendar/CalendarEvent.ts +16 -0
  315. package/Types/Cloud/CloudPlatform.ts +372 -0
  316. package/Types/CodeRepository/GitHubCommand.ts +69 -0
  317. package/Types/Dashboard/DashboardComponentType.ts +2 -0
  318. package/Types/Dashboard/DashboardComponents/ComponentArgument.ts +1 -0
  319. package/Types/Dashboard/DashboardComponents/DashboardVMwareHostListComponent.ts +23 -0
  320. package/Types/Dashboard/DashboardComponents/DashboardVMwareVirtualMachineListComponent.ts +30 -0
  321. package/Types/Dashboard/DashboardLanguage.ts +1 -0
  322. package/Types/Dashboard/DashboardTemplates.ts +314 -0
  323. package/Types/Docs/DocsLanguage.ts +1 -0
  324. package/Types/Exception/EgressGuardException.ts +65 -0
  325. package/Types/Icon/IconProp.ts +1 -0
  326. package/Types/Monitor/MetricMonitor/MetricMonitorResponse.ts +36 -0
  327. package/Types/Monitor/MonitorCriteriaInstance.ts +1 -0
  328. package/Types/Monitor/MonitorStep.ts +78 -17
  329. package/Types/Monitor/MonitorStepMetricViewConfigUtil.ts +1 -0
  330. package/Types/Monitor/MonitorStepVMwareMonitor.ts +127 -0
  331. package/Types/Monitor/MonitorTemplateSyncField.ts +291 -0
  332. package/Types/Monitor/MonitorType.ts +24 -0
  333. package/Types/Monitor/Recommendation/MonitorRecommendationCatalog.ts +33 -0
  334. package/Types/Monitor/Recommendation/MonitorRecommendationTypes.ts +2 -0
  335. package/Types/Monitor/Recommendation/MonitorRecommendationUtil.ts +3 -0
  336. package/Types/Monitor/SeriesContext/SeriesDebugHints.ts +158 -0
  337. package/Types/Monitor/SeriesContext/SeriesLabelDisplay.ts +61 -0
  338. package/Types/Monitor/VMwareAlertTemplates.ts +1070 -0
  339. package/Types/Monitor/VMwareMetricCatalog.ts +914 -0
  340. package/Types/OnCallDutyPolicy/ScheduleShiftUtil.ts +29 -0
  341. package/Types/OnCallDutyPolicy/UserOverrideUtil.ts +16 -0
  342. package/Types/Password.ts +84 -5
  343. package/Types/Permission.ts +265 -0
  344. package/Types/Probe/RequestFailedDetails.ts +7 -0
  345. package/Types/Runner/RunnerLiveStatus.ts +1 -1
  346. package/Types/SecurityEvent/GoogleSecOpsDiagnostics.ts +56 -0
  347. package/Types/StatusPage/StatusPageLanguage.ts +1 -0
  348. package/Types/Telemetry/EntityType.ts +21 -0
  349. package/Types/Telemetry/ServiceType.ts +1 -0
  350. package/Typings/Index.d.ts +63 -0
  351. package/UI/Components/Breadcrumbs/Breadcrumbs.tsx +2 -2
  352. package/UI/Components/Calendar/Calendar.css +78 -0
  353. package/UI/Components/Calendar/Calendar.tsx +28 -3
  354. package/UI/Components/Forms/BasicForm.tsx +44 -2
  355. package/UI/Components/Forms/Fields/ColorPicker.tsx +2 -1
  356. package/UI/Components/Forms/Fields/FormField.tsx +16 -3
  357. package/UI/Components/Forms/ModelForm.tsx +159 -5
  358. package/UI/Components/Forms/Types/Field.ts +8 -1
  359. package/UI/Components/Graphs/DayUptimeGraph.tsx +8 -10
  360. package/UI/Components/Icon/Icon.tsx +16 -0
  361. package/UI/Components/Input/Input.tsx +42 -7
  362. package/UI/Components/LabelRule/ImportLabelRulesModal.tsx +441 -0
  363. package/UI/Components/LabelRule/LabelRuleTable.tsx +142 -0
  364. package/UI/Components/ModelTable/BaseModelTable.tsx +22 -5
  365. package/UI/Components/ModelTable/ModelTable.tsx +14 -6
  366. package/UI/Components/MonitorGraphs/Uptime.tsx +17 -27
  367. package/UI/Components/MonitorTemplateVariables/TemplateVariablesCatalog.ts +3 -1
  368. package/UI/Components/Tooltip/Tooltip.tsx +52 -6
  369. package/UI/Utils/DashboardStackingLayers.ts +85 -0
  370. package/UI/Utils/LabelRuleImportExport.ts +310 -0
  371. package/UI/Utils/ModelAPI/ModelAPI.ts +4 -3
  372. package/UI/Utils/TelemetryService.ts +4 -3
  373. package/UI/Utils/UseDashboardGridDnd.ts +4 -1
  374. package/UI/Utils/UserEmailRegistrationStatus.ts +33 -5
  375. package/UI/Utils/WebAuthn.ts +220 -0
  376. package/UI/esbuild-config.js +4 -0
  377. package/Utils/API.ts +34 -0
  378. package/Utils/Dashboard/Components/DashboardVMwareHostListComponent.ts +38 -0
  379. package/Utils/Dashboard/Components/DashboardVMwareResourceListShared.ts +70 -0
  380. package/Utils/Dashboard/Components/DashboardVMwareVirtualMachineListComponent.ts +76 -0
  381. package/Utils/Dashboard/Components/Index.ts +16 -0
  382. package/Utils/LabelRuleImportExport.ts +496 -0
  383. package/Utils/Monitor/MonitorTemplateSyncUtil.ts +164 -0
  384. package/Utils/NetworkDiscovery/DiscoveryScanStatus.ts +8 -0
  385. package/Utils/Telemetry/CloudInstanceIdentity.ts +94 -0
  386. package/Utils/Telemetry/EntityKey.ts +19 -0
  387. package/Utils/Telemetry/EntityRelationship.ts +19 -0
  388. package/build/dist/Models/DatabaseModels/Alert.js +54 -0
  389. package/build/dist/Models/DatabaseModels/Alert.js.map +1 -1
  390. package/build/dist/Models/DatabaseModels/CloudResource.js +44 -7
  391. package/build/dist/Models/DatabaseModels/CloudResource.js.map +1 -1
  392. package/build/dist/Models/DatabaseModels/CodeRepository.js +87 -0
  393. package/build/dist/Models/DatabaseModels/CodeRepository.js.map +1 -1
  394. package/build/dist/Models/DatabaseModels/GoogleSecOpsConnection.js +60 -7
  395. package/build/dist/Models/DatabaseModels/GoogleSecOpsConnection.js.map +1 -1
  396. package/build/dist/Models/DatabaseModels/GoogleSecOpsConnectionRun.js +287 -0
  397. package/build/dist/Models/DatabaseModels/GoogleSecOpsConnectionRun.js.map +1 -0
  398. package/build/dist/Models/DatabaseModels/Incident.js +54 -0
  399. package/build/dist/Models/DatabaseModels/Incident.js.map +1 -1
  400. package/build/dist/Models/DatabaseModels/Index.js +16 -0
  401. package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
  402. package/build/dist/Models/DatabaseModels/ScheduledMaintenance.js +54 -0
  403. package/build/dist/Models/DatabaseModels/ScheduledMaintenance.js.map +1 -1
  404. package/build/dist/Models/DatabaseModels/User.js +57 -4
  405. package/build/dist/Models/DatabaseModels/User.js.map +1 -1
  406. package/build/dist/Models/DatabaseModels/UserNotificationEmailRollupItem.js +47 -2
  407. package/build/dist/Models/DatabaseModels/UserNotificationEmailRollupItem.js.map +1 -1
  408. package/build/dist/Models/DatabaseModels/UserWebAuthn.js +18 -0
  409. package/build/dist/Models/DatabaseModels/UserWebAuthn.js.map +1 -1
  410. package/build/dist/Models/DatabaseModels/VMwareResource.js +1201 -0
  411. package/build/dist/Models/DatabaseModels/VMwareResource.js.map +1 -0
  412. package/build/dist/Models/DatabaseModels/VMwareVCenter.js +1149 -0
  413. package/build/dist/Models/DatabaseModels/VMwareVCenter.js.map +1 -0
  414. package/build/dist/Models/DatabaseModels/VMwareVCenterFeed.js +648 -0
  415. package/build/dist/Models/DatabaseModels/VMwareVCenterFeed.js.map +1 -0
  416. package/build/dist/Models/DatabaseModels/VMwareVCenterLabelRule.js +522 -0
  417. package/build/dist/Models/DatabaseModels/VMwareVCenterLabelRule.js.map +1 -0
  418. package/build/dist/Models/DatabaseModels/VMwareVCenterOwnerRule.js +603 -0
  419. package/build/dist/Models/DatabaseModels/VMwareVCenterOwnerRule.js.map +1 -0
  420. package/build/dist/Models/DatabaseModels/VMwareVCenterOwnerTeam.js +503 -0
  421. package/build/dist/Models/DatabaseModels/VMwareVCenterOwnerTeam.js.map +1 -0
  422. package/build/dist/Models/DatabaseModels/VMwareVCenterOwnerUser.js +502 -0
  423. package/build/dist/Models/DatabaseModels/VMwareVCenterOwnerUser.js.map +1 -0
  424. package/build/dist/Models/DatabaseModels/WorkflowVariable.js +22 -1
  425. package/build/dist/Models/DatabaseModels/WorkflowVariable.js.map +1 -1
  426. package/build/dist/Server/API/AIAgentDataAPI.js +63 -3
  427. package/build/dist/Server/API/AIAgentDataAPI.js.map +1 -1
  428. package/build/dist/Server/API/AIAgentGitHubAPI.js +363 -0
  429. package/build/dist/Server/API/AIAgentGitHubAPI.js.map +1 -0
  430. package/build/dist/Server/API/AIChatAPI.js +19 -1
  431. package/build/dist/Server/API/AIChatAPI.js.map +1 -1
  432. package/build/dist/Server/API/BillingAPI.js +22 -0
  433. package/build/dist/Server/API/BillingAPI.js.map +1 -1
  434. package/build/dist/Server/API/DashboardAPI.js +10 -0
  435. package/build/dist/Server/API/DashboardAPI.js.map +1 -1
  436. package/build/dist/Server/API/GitHubAPI.js +33 -5
  437. package/build/dist/Server/API/GitHubAPI.js.map +1 -1
  438. package/build/dist/Server/API/GoogleSecOpsConnectionAPI.js +56 -0
  439. package/build/dist/Server/API/GoogleSecOpsConnectionAPI.js.map +1 -0
  440. package/build/dist/Server/API/UserWebAuthnAPI.js +17 -1
  441. package/build/dist/Server/API/UserWebAuthnAPI.js.map +1 -1
  442. package/build/dist/Server/API/VMwareResourceAPI.js +106 -0
  443. package/build/dist/Server/API/VMwareResourceAPI.js.map +1 -0
  444. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1791800000000-SplitWebAuthnChallengeByPurpose.js +41 -0
  445. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1791800000000-SplitWebAuthnChallengeByPurpose.js.map +1 -0
  446. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1791900000000-AddSeverityAndStateToNotificationEmailRollup.js +14 -0
  447. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1791900000000-AddSeverityAndStateToNotificationEmailRollup.js.map +1 -0
  448. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1792000000000-AddVMwareTables.js +241 -0
  449. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1792000000000-AddVMwareTables.js.map +1 -0
  450. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1792100000000-AddGitHubCommandSettingsToCodeRepository.js +36 -0
  451. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1792100000000-AddGitHubCommandSettingsToCodeRepository.js.map +1 -0
  452. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1792200000000-AddUserWebAuthnPurpose.js +12 -0
  453. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1792200000000-AddUserWebAuthnPurpose.js.map +1 -0
  454. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1792300000000-AddGoogleSecOpsDiagnostics.js +34 -0
  455. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1792300000000-AddGoogleSecOpsDiagnostics.js.map +1 -0
  456. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +12 -0
  457. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  458. package/build/dist/Server/Middleware/IdentityRateLimit.js +80 -0
  459. package/build/dist/Server/Middleware/IdentityRateLimit.js.map +1 -1
  460. package/build/dist/Server/Services/AIRunService.js +14 -1
  461. package/build/dist/Server/Services/AIRunService.js.map +1 -1
  462. package/build/dist/Server/Services/BillingPaymentMethodService.js +71 -21
  463. package/build/dist/Server/Services/BillingPaymentMethodService.js.map +1 -1
  464. package/build/dist/Server/Services/BillingService.js +210 -22
  465. package/build/dist/Server/Services/BillingService.js.map +1 -1
  466. package/build/dist/Server/Services/CloudResourceInstanceService.js +66 -0
  467. package/build/dist/Server/Services/CloudResourceInstanceService.js.map +1 -1
  468. package/build/dist/Server/Services/ExceptionAggregationService.js +1 -0
  469. package/build/dist/Server/Services/ExceptionAggregationService.js.map +1 -1
  470. package/build/dist/Server/Services/GoogleSecOpsConnectionRunService.js +9 -0
  471. package/build/dist/Server/Services/GoogleSecOpsConnectionRunService.js.map +1 -0
  472. package/build/dist/Server/Services/GoogleSecOpsConnectionService.js +7 -0
  473. package/build/dist/Server/Services/GoogleSecOpsConnectionService.js.map +1 -1
  474. package/build/dist/Server/Services/Index.js +16 -0
  475. package/build/dist/Server/Services/Index.js.map +1 -1
  476. package/build/dist/Server/Services/LogAggregationService.js +1 -0
  477. package/build/dist/Server/Services/LogAggregationService.js.map +1 -1
  478. package/build/dist/Server/Services/MetricAggregationService.js +1 -0
  479. package/build/dist/Server/Services/MetricAggregationService.js.map +1 -1
  480. package/build/dist/Server/Services/MobilePasskeyLoginService.js +137 -0
  481. package/build/dist/Server/Services/MobilePasskeyLoginService.js.map +1 -0
  482. package/build/dist/Server/Services/MonitorService.js +34 -1
  483. package/build/dist/Server/Services/MonitorService.js.map +1 -1
  484. package/build/dist/Server/Services/MonitorTemplateService.js +64 -26
  485. package/build/dist/Server/Services/MonitorTemplateService.js.map +1 -1
  486. package/build/dist/Server/Services/NetworkDeviceAutoImportRuleEngineService.js +124 -1
  487. package/build/dist/Server/Services/NetworkDeviceAutoImportRuleEngineService.js.map +1 -1
  488. package/build/dist/Server/Services/NetworkDeviceAutoImportRuleService.js +142 -2
  489. package/build/dist/Server/Services/NetworkDeviceAutoImportRuleService.js.map +1 -1
  490. package/build/dist/Server/Services/OpenTelemetryIngestService.js +28 -13
  491. package/build/dist/Server/Services/OpenTelemetryIngestService.js.map +1 -1
  492. package/build/dist/Server/Services/PayAsYouGoBillingService.js +297 -0
  493. package/build/dist/Server/Services/PayAsYouGoBillingService.js.map +1 -0
  494. package/build/dist/Server/Services/ProjectService.js +38 -2
  495. package/build/dist/Server/Services/ProjectService.js.map +1 -1
  496. package/build/dist/Server/Services/ServerlessFunctionInstanceService.js +66 -0
  497. package/build/dist/Server/Services/ServerlessFunctionInstanceService.js.map +1 -1
  498. package/build/dist/Server/Services/TeamMemberService.js +85 -28
  499. package/build/dist/Server/Services/TeamMemberService.js.map +1 -1
  500. package/build/dist/Server/Services/TelemetryIngestionKeyService.js +61 -10
  501. package/build/dist/Server/Services/TelemetryIngestionKeyService.js.map +1 -1
  502. package/build/dist/Server/Services/TelemetryUsageBillingService.js +62 -5
  503. package/build/dist/Server/Services/TelemetryUsageBillingService.js.map +1 -1
  504. package/build/dist/Server/Services/TraceAggregationService.js +5 -4
  505. package/build/dist/Server/Services/TraceAggregationService.js.map +1 -1
  506. package/build/dist/Server/Services/UserWebAuthnService.js +345 -113
  507. package/build/dist/Server/Services/UserWebAuthnService.js.map +1 -1
  508. package/build/dist/Server/Services/VMwareResourceService.js +395 -0
  509. package/build/dist/Server/Services/VMwareResourceService.js.map +1 -0
  510. package/build/dist/Server/Services/VMwareVCenterFeedService.js +89 -0
  511. package/build/dist/Server/Services/VMwareVCenterFeedService.js.map +1 -0
  512. package/build/dist/Server/Services/VMwareVCenterLabelRuleEngineService.js +197 -0
  513. package/build/dist/Server/Services/VMwareVCenterLabelRuleEngineService.js.map +1 -0
  514. package/build/dist/Server/Services/VMwareVCenterLabelRuleService.js +13 -0
  515. package/build/dist/Server/Services/VMwareVCenterLabelRuleService.js.map +1 -0
  516. package/build/dist/Server/Services/VMwareVCenterOwnerRuleEngineService.js +214 -0
  517. package/build/dist/Server/Services/VMwareVCenterOwnerRuleEngineService.js.map +1 -0
  518. package/build/dist/Server/Services/VMwareVCenterOwnerRuleService.js +13 -0
  519. package/build/dist/Server/Services/VMwareVCenterOwnerRuleService.js.map +1 -0
  520. package/build/dist/Server/Services/VMwareVCenterOwnerTeamService.js +129 -0
  521. package/build/dist/Server/Services/VMwareVCenterOwnerTeamService.js.map +1 -0
  522. package/build/dist/Server/Services/VMwareVCenterOwnerUserService.js +122 -0
  523. package/build/dist/Server/Services/VMwareVCenterOwnerUserService.js.map +1 -0
  524. package/build/dist/Server/Services/VMwareVCenterService.js +533 -0
  525. package/build/dist/Server/Services/VMwareVCenterService.js.map +1 -0
  526. package/build/dist/Server/Services/WorkflowVariableService.js +169 -0
  527. package/build/dist/Server/Services/WorkflowVariableService.js.map +1 -1
  528. package/build/dist/Server/Types/Billing/MeteredPlan/ActiveMonitoringMeteredPlan.js +12 -1
  529. package/build/dist/Server/Types/Billing/MeteredPlan/ActiveMonitoringMeteredPlan.js.map +1 -1
  530. package/build/dist/Server/Types/Billing/MeteredPlan/TelemetryMeteredPlan.js +25 -2
  531. package/build/dist/Server/Types/Billing/MeteredPlan/TelemetryMeteredPlan.js.map +1 -1
  532. package/build/dist/Server/Utils/CodeRepository/GitHub/GitHub.js.map +1 -1
  533. package/build/dist/Server/Utils/CodeRepository/GitHub/GitHubAgentTaskTrigger.js +275 -0
  534. package/build/dist/Server/Utils/CodeRepository/GitHub/GitHubAgentTaskTrigger.js.map +1 -0
  535. package/build/dist/Server/Utils/CodeRepository/GitHub/GitHubCommandAuthorizer.js +226 -0
  536. package/build/dist/Server/Utils/CodeRepository/GitHub/GitHubCommandAuthorizer.js.map +1 -0
  537. package/build/dist/Server/Utils/CodeRepository/GitHub/GitHubCommandParser.js +297 -0
  538. package/build/dist/Server/Utils/CodeRepository/GitHub/GitHubCommandParser.js.map +1 -0
  539. package/build/dist/Server/Utils/CodeRepository/GitHub/GitHubConversation.js +582 -0
  540. package/build/dist/Server/Utils/CodeRepository/GitHub/GitHubConversation.js.map +1 -0
  541. package/build/dist/Server/Utils/CodeRepository/GitHub/GitHubReplyComposer.js +275 -0
  542. package/build/dist/Server/Utils/CodeRepository/GitHub/GitHubReplyComposer.js.map +1 -0
  543. package/build/dist/Server/Utils/CodeRepository/GitHub/GitHubRunReply.js +365 -0
  544. package/build/dist/Server/Utils/CodeRepository/GitHub/GitHubRunReply.js.map +1 -0
  545. package/build/dist/Server/Utils/CodeRepository/GitHub/GitHubWebhookEvents.js +103 -0
  546. package/build/dist/Server/Utils/CodeRepository/GitHub/GitHubWebhookEvents.js.map +1 -0
  547. package/build/dist/Server/Utils/CodeRepository/GitHub/GitHubWebhookHandler.js +661 -0
  548. package/build/dist/Server/Utils/CodeRepository/GitHub/GitHubWebhookHandler.js.map +1 -0
  549. package/build/dist/Server/Utils/Dashboard/PublicDashboardResourceListPolicy.js +113 -0
  550. package/build/dist/Server/Utils/Dashboard/PublicDashboardResourceListPolicy.js.map +1 -1
  551. package/build/dist/Server/Utils/DataSource/EgressGuard.js +99 -15
  552. package/build/dist/Server/Utils/DataSource/EgressGuard.js.map +1 -1
  553. package/build/dist/Server/Utils/EmailRollup/EmailRollupFlushRunner.js +2 -0
  554. package/build/dist/Server/Utils/EmailRollup/EmailRollupFlushRunner.js.map +1 -1
  555. package/build/dist/Server/Utils/EmailRollup/EmailRollupRenderer.js +18 -0
  556. package/build/dist/Server/Utils/EmailRollup/EmailRollupRenderer.js.map +1 -1
  557. package/build/dist/Server/Utils/EmailRollup/EmailRollupWriter.js +31 -2
  558. package/build/dist/Server/Utils/EmailRollup/EmailRollupWriter.js.map +1 -1
  559. package/build/dist/Server/Utils/LogRedaction.js +2 -1
  560. package/build/dist/Server/Utils/LogRedaction.js.map +1 -1
  561. package/build/dist/Server/Utils/Monitor/MonitorAlert.js +1 -1
  562. package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js +219 -4
  563. package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js.map +1 -1
  564. package/build/dist/Server/Utils/Monitor/MonitorMaintenanceSuppression.js +12 -4
  565. package/build/dist/Server/Utils/Monitor/MonitorMaintenanceSuppression.js.map +1 -1
  566. package/build/dist/Server/Utils/Monitor/MonitorResourceContext.js +5 -3
  567. package/build/dist/Server/Utils/Monitor/MonitorResourceContext.js.map +1 -1
  568. package/build/dist/Server/Utils/Monitor/MonitorStepResourceIdentity.js +22 -9
  569. package/build/dist/Server/Utils/Monitor/MonitorStepResourceIdentity.js.map +1 -1
  570. package/build/dist/Server/Utils/Monitor/MonitorTemplateUtil.js +1 -0
  571. package/build/dist/Server/Utils/Monitor/MonitorTemplateUtil.js.map +1 -1
  572. package/build/dist/Server/Utils/Monitor/SeriesResourceLabels.js +21 -0
  573. package/build/dist/Server/Utils/Monitor/SeriesResourceLabels.js.map +1 -1
  574. package/build/dist/Server/Utils/Monitor/SeriesResourceLinker.js +21 -6
  575. package/build/dist/Server/Utils/Monitor/SeriesResourceLinker.js.map +1 -1
  576. package/build/dist/Server/Utils/ResourceFeed/ResourceFeedUtil.js +3 -3
  577. package/build/dist/Server/Utils/SecurityEvent/ConnectorErrorMessage.js +9 -9
  578. package/build/dist/Server/Utils/SecurityEvent/ConnectorErrorMessage.js.map +1 -1
  579. package/build/dist/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsClient.js +92 -71
  580. package/build/dist/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsClient.js.map +1 -1
  581. package/build/dist/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsPoller.js +428 -143
  582. package/build/dist/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsPoller.js.map +1 -1
  583. package/build/dist/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsRunExecutor.js +376 -0
  584. package/build/dist/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsRunExecutor.js.map +1 -0
  585. package/build/dist/Server/Utils/Telemetry/EntityRegistry.js +5 -0
  586. package/build/dist/Server/Utils/Telemetry/EntityRegistry.js.map +1 -1
  587. package/build/dist/Server/Utils/Telemetry/ResourceHeartbeat.js.map +1 -1
  588. package/build/dist/Server/Utils/Telemetry/TelemetryEntity.js +136 -1
  589. package/build/dist/Server/Utils/Telemetry/TelemetryEntity.js.map +1 -1
  590. package/build/dist/Server/Utils/Telemetry/VMwareSnapshotScan.js +933 -0
  591. package/build/dist/Server/Utils/Telemetry/VMwareSnapshotScan.js.map +1 -0
  592. package/build/dist/Types/AI/CodeFixTaskContext.js +40 -0
  593. package/build/dist/Types/AI/CodeFixTaskContext.js.map +1 -1
  594. package/build/dist/Types/AI/CodeFixTaskType.js +57 -0
  595. package/build/dist/Types/AI/CodeFixTaskType.js.map +1 -1
  596. package/build/dist/Types/Accounts/AccountsLanguage.js +1 -0
  597. package/build/dist/Types/Accounts/AccountsLanguage.js.map +1 -1
  598. package/build/dist/Types/AdminDashboard/AdminDashboardLanguage.js +1 -0
  599. package/build/dist/Types/AdminDashboard/AdminDashboardLanguage.js.map +1 -1
  600. package/build/dist/Types/Cloud/CloudPlatform.js +277 -0
  601. package/build/dist/Types/Cloud/CloudPlatform.js.map +1 -0
  602. package/build/dist/Types/CodeRepository/GitHubCommand.js +52 -0
  603. package/build/dist/Types/CodeRepository/GitHubCommand.js.map +1 -0
  604. package/build/dist/Types/Dashboard/DashboardComponentType.js +2 -0
  605. package/build/dist/Types/Dashboard/DashboardComponentType.js.map +1 -1
  606. package/build/dist/Types/Dashboard/DashboardComponents/ComponentArgument.js +1 -0
  607. package/build/dist/Types/Dashboard/DashboardComponents/ComponentArgument.js.map +1 -1
  608. package/build/dist/Types/Dashboard/DashboardComponents/DashboardVMwareHostListComponent.js +2 -0
  609. package/build/dist/Types/Dashboard/DashboardComponents/DashboardVMwareHostListComponent.js.map +1 -0
  610. package/build/dist/Types/Dashboard/DashboardComponents/DashboardVMwareVirtualMachineListComponent.js +2 -0
  611. package/build/dist/Types/Dashboard/DashboardComponents/DashboardVMwareVirtualMachineListComponent.js.map +1 -0
  612. package/build/dist/Types/Dashboard/DashboardLanguage.js +1 -0
  613. package/build/dist/Types/Dashboard/DashboardLanguage.js.map +1 -1
  614. package/build/dist/Types/Dashboard/DashboardTemplates.js +286 -0
  615. package/build/dist/Types/Dashboard/DashboardTemplates.js.map +1 -1
  616. package/build/dist/Types/Docs/DocsLanguage.js +1 -0
  617. package/build/dist/Types/Docs/DocsLanguage.js.map +1 -1
  618. package/build/dist/Types/Exception/EgressGuardException.js +59 -0
  619. package/build/dist/Types/Exception/EgressGuardException.js.map +1 -0
  620. package/build/dist/Types/Icon/IconProp.js +1 -0
  621. package/build/dist/Types/Icon/IconProp.js.map +1 -1
  622. package/build/dist/Types/Monitor/MonitorCriteriaInstance.js +1 -0
  623. package/build/dist/Types/Monitor/MonitorCriteriaInstance.js.map +1 -1
  624. package/build/dist/Types/Monitor/MonitorStep.js +61 -18
  625. package/build/dist/Types/Monitor/MonitorStep.js.map +1 -1
  626. package/build/dist/Types/Monitor/MonitorStepMetricViewConfigUtil.js +1 -0
  627. package/build/dist/Types/Monitor/MonitorStepMetricViewConfigUtil.js.map +1 -1
  628. package/build/dist/Types/Monitor/MonitorStepVMwareMonitor.js +71 -0
  629. package/build/dist/Types/Monitor/MonitorStepVMwareMonitor.js.map +1 -0
  630. package/build/dist/Types/Monitor/MonitorTemplateSyncField.js +211 -0
  631. package/build/dist/Types/Monitor/MonitorTemplateSyncField.js.map +1 -0
  632. package/build/dist/Types/Monitor/MonitorType.js +23 -0
  633. package/build/dist/Types/Monitor/MonitorType.js.map +1 -1
  634. package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationCatalog.js +28 -0
  635. package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationCatalog.js.map +1 -1
  636. package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationTypes.js +1 -0
  637. package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationTypes.js.map +1 -1
  638. package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationUtil.js +2 -0
  639. package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationUtil.js.map +1 -1
  640. package/build/dist/Types/Monitor/SeriesContext/SeriesDebugHints.js +120 -0
  641. package/build/dist/Types/Monitor/SeriesContext/SeriesDebugHints.js.map +1 -1
  642. package/build/dist/Types/Monitor/SeriesContext/SeriesLabelDisplay.js +60 -0
  643. package/build/dist/Types/Monitor/SeriesContext/SeriesLabelDisplay.js.map +1 -1
  644. package/build/dist/Types/Monitor/VMwareAlertTemplates.js +775 -0
  645. package/build/dist/Types/Monitor/VMwareAlertTemplates.js.map +1 -0
  646. package/build/dist/Types/Monitor/VMwareMetricCatalog.js +799 -0
  647. package/build/dist/Types/Monitor/VMwareMetricCatalog.js.map +1 -0
  648. package/build/dist/Types/OnCallDutyPolicy/ScheduleShiftUtil.js +25 -0
  649. package/build/dist/Types/OnCallDutyPolicy/ScheduleShiftUtil.js.map +1 -1
  650. package/build/dist/Types/OnCallDutyPolicy/UserOverrideUtil.js +5 -0
  651. package/build/dist/Types/OnCallDutyPolicy/UserOverrideUtil.js.map +1 -1
  652. package/build/dist/Types/Password.js +63 -5
  653. package/build/dist/Types/Password.js.map +1 -1
  654. package/build/dist/Types/Permission.js +234 -0
  655. package/build/dist/Types/Permission.js.map +1 -1
  656. package/build/dist/Types/Probe/RequestFailedDetails.js +7 -0
  657. package/build/dist/Types/Probe/RequestFailedDetails.js.map +1 -1
  658. package/build/dist/Types/SecurityEvent/GoogleSecOpsDiagnostics.js +2 -0
  659. package/build/dist/Types/SecurityEvent/GoogleSecOpsDiagnostics.js.map +1 -0
  660. package/build/dist/Types/StatusPage/StatusPageLanguage.js +1 -0
  661. package/build/dist/Types/StatusPage/StatusPageLanguage.js.map +1 -1
  662. package/build/dist/Types/Telemetry/EntityType.js +21 -0
  663. package/build/dist/Types/Telemetry/EntityType.js.map +1 -1
  664. package/build/dist/Types/Telemetry/ServiceType.js +1 -0
  665. package/build/dist/Types/Telemetry/ServiceType.js.map +1 -1
  666. package/build/dist/UI/Components/Breadcrumbs/Breadcrumbs.js +2 -2
  667. package/build/dist/UI/Components/Breadcrumbs/Breadcrumbs.js.map +1 -1
  668. package/build/dist/UI/Components/Calendar/Calendar.js +24 -4
  669. package/build/dist/UI/Components/Calendar/Calendar.js.map +1 -1
  670. package/build/dist/UI/Components/Forms/BasicForm.js +28 -3
  671. package/build/dist/UI/Components/Forms/BasicForm.js.map +1 -1
  672. package/build/dist/UI/Components/Forms/Fields/ColorPicker.js +1 -1
  673. package/build/dist/UI/Components/Forms/Fields/ColorPicker.js.map +1 -1
  674. package/build/dist/UI/Components/Forms/Fields/FormField.js +10 -3
  675. package/build/dist/UI/Components/Forms/Fields/FormField.js.map +1 -1
  676. package/build/dist/UI/Components/Forms/ModelForm.js +89 -6
  677. package/build/dist/UI/Components/Forms/ModelForm.js.map +1 -1
  678. package/build/dist/UI/Components/Graphs/DayUptimeGraph.js +5 -6
  679. package/build/dist/UI/Components/Graphs/DayUptimeGraph.js.map +1 -1
  680. package/build/dist/UI/Components/Icon/Icon.js +14 -0
  681. package/build/dist/UI/Components/Icon/Icon.js.map +1 -1
  682. package/build/dist/UI/Components/Input/Input.js +37 -8
  683. package/build/dist/UI/Components/Input/Input.js.map +1 -1
  684. package/build/dist/UI/Components/LabelRule/ImportLabelRulesModal.js +228 -0
  685. package/build/dist/UI/Components/LabelRule/ImportLabelRulesModal.js.map +1 -0
  686. package/build/dist/UI/Components/LabelRule/LabelRuleTable.js +102 -0
  687. package/build/dist/UI/Components/LabelRule/LabelRuleTable.js.map +1 -0
  688. package/build/dist/UI/Components/ModelTable/BaseModelTable.js +15 -2
  689. package/build/dist/UI/Components/ModelTable/BaseModelTable.js.map +1 -1
  690. package/build/dist/UI/Components/ModelTable/ModelTable.js +13 -5
  691. package/build/dist/UI/Components/ModelTable/ModelTable.js.map +1 -1
  692. package/build/dist/UI/Components/MonitorGraphs/Uptime.js +14 -15
  693. package/build/dist/UI/Components/MonitorGraphs/Uptime.js.map +1 -1
  694. package/build/dist/UI/Components/MonitorTemplateVariables/TemplateVariablesCatalog.js +2 -0
  695. package/build/dist/UI/Components/MonitorTemplateVariables/TemplateVariablesCatalog.js.map +1 -1
  696. package/build/dist/UI/Components/Tooltip/Tooltip.js +33 -4
  697. package/build/dist/UI/Components/Tooltip/Tooltip.js.map +1 -1
  698. package/build/dist/UI/Utils/DashboardStackingLayers.js +44 -0
  699. package/build/dist/UI/Utils/DashboardStackingLayers.js.map +1 -0
  700. package/build/dist/UI/Utils/LabelRuleImportExport.js +195 -0
  701. package/build/dist/UI/Utils/LabelRuleImportExport.js.map +1 -0
  702. package/build/dist/UI/Utils/ModelAPI/ModelAPI.js +4 -4
  703. package/build/dist/UI/Utils/ModelAPI/ModelAPI.js.map +1 -1
  704. package/build/dist/UI/Utils/TelemetryService.js +3 -2
  705. package/build/dist/UI/Utils/TelemetryService.js.map +1 -1
  706. package/build/dist/UI/Utils/UseDashboardGridDnd.js +2 -1
  707. package/build/dist/UI/Utils/UseDashboardGridDnd.js.map +1 -1
  708. package/build/dist/UI/Utils/UserEmailRegistrationStatus.js +21 -4
  709. package/build/dist/UI/Utils/UserEmailRegistrationStatus.js.map +1 -1
  710. package/build/dist/UI/Utils/WebAuthn.js +121 -0
  711. package/build/dist/UI/Utils/WebAuthn.js.map +1 -0
  712. package/build/dist/Utils/API.js +29 -0
  713. package/build/dist/Utils/API.js.map +1 -1
  714. package/build/dist/Utils/Dashboard/Components/DashboardVMwareHostListComponent.js +33 -0
  715. package/build/dist/Utils/Dashboard/Components/DashboardVMwareHostListComponent.js.map +1 -0
  716. package/build/dist/Utils/Dashboard/Components/DashboardVMwareResourceListShared.js +54 -0
  717. package/build/dist/Utils/Dashboard/Components/DashboardVMwareResourceListShared.js.map +1 -0
  718. package/build/dist/Utils/Dashboard/Components/DashboardVMwareVirtualMachineListComponent.js +60 -0
  719. package/build/dist/Utils/Dashboard/Components/DashboardVMwareVirtualMachineListComponent.js.map +1 -0
  720. package/build/dist/Utils/Dashboard/Components/Index.js +8 -0
  721. package/build/dist/Utils/Dashboard/Components/Index.js.map +1 -1
  722. package/build/dist/Utils/LabelRuleImportExport.js +356 -0
  723. package/build/dist/Utils/LabelRuleImportExport.js.map +1 -0
  724. package/build/dist/Utils/Monitor/MonitorTemplateSyncUtil.js +110 -0
  725. package/build/dist/Utils/Monitor/MonitorTemplateSyncUtil.js.map +1 -0
  726. package/build/dist/Utils/NetworkDiscovery/DiscoveryScanStatus.js +6 -0
  727. package/build/dist/Utils/NetworkDiscovery/DiscoveryScanStatus.js.map +1 -1
  728. package/build/dist/Utils/Telemetry/CloudInstanceIdentity.js +88 -0
  729. package/build/dist/Utils/Telemetry/CloudInstanceIdentity.js.map +1 -0
  730. package/build/dist/Utils/Telemetry/EntityKey.js +15 -0
  731. package/build/dist/Utils/Telemetry/EntityKey.js.map +1 -1
  732. package/build/dist/Utils/Telemetry/EntityRelationship.js +13 -0
  733. package/build/dist/Utils/Telemetry/EntityRelationship.js.map +1 -1
  734. package/package.json +1 -1
@@ -0,0 +1,2202 @@
1
+ import GitHubWebhookHandler, {
2
+ GitHubWebhookHandlingResult,
3
+ } from "../../../../Server/Utils/CodeRepository/GitHub/GitHubWebhookHandler";
4
+ import GitHubAgentTaskTrigger from "../../../../Server/Utils/CodeRepository/GitHub/GitHubAgentTaskTrigger";
5
+ import GitHubCommandAuthorizer, {
6
+ GitHubAuthorizationOutcome,
7
+ GitHubAuthorizationResult,
8
+ } from "../../../../Server/Utils/CodeRepository/GitHub/GitHubCommandAuthorizer";
9
+ import GitHubCommandParser from "../../../../Server/Utils/CodeRepository/GitHub/GitHubCommandParser";
10
+ import GitHubConversation, {
11
+ GitHubPullRequestDetails,
12
+ GitHubReaction,
13
+ } from "../../../../Server/Utils/CodeRepository/GitHub/GitHubConversation";
14
+ import GitHubRunReply from "../../../../Server/Utils/CodeRepository/GitHub/GitHubRunReply";
15
+ import GlobalCache from "../../../../Server/Infrastructure/GlobalCache";
16
+ import logger from "../../../../Server/Utils/Logger";
17
+ import AIRun from "../../../../Models/DatabaseModels/AIRun";
18
+ import CodeRepository from "../../../../Models/DatabaseModels/CodeRepository";
19
+ import AIRunStatus from "../../../../Types/AI/AIRunStatus";
20
+ import CodeFixTaskType from "../../../../Types/AI/CodeFixTaskType";
21
+ import { GitHubTaskContext } from "../../../../Types/AI/CodeFixTaskContext";
22
+ import GitHubCommandType, {
23
+ GitHubCommandSurface,
24
+ } from "../../../../Types/CodeRepository/GitHubCommand";
25
+ import BadDataException from "../../../../Types/Exception/BadDataException";
26
+ import { JSONObject } from "../../../../Types/JSON";
27
+ import ObjectID from "../../../../Types/ObjectID";
28
+ import { afterEach, beforeEach, describe, expect, test } from "@jest/globals";
29
+
30
+ /*
31
+ * ---------------------------------------------------------------------------
32
+ * The decision tree between a VERIFIED GitHub webhook and a queued agent run.
33
+ *
34
+ * The route above this only checks the signature and answers 200; everything
35
+ * that decides whether an arbitrary comment in somebody else's repository gets
36
+ * to spend an AI budget, push commits, or make this app write a comment lives
37
+ * here. Four invariants are worth failing a build over, and each of them is a
38
+ * real incident if it regresses:
39
+ *
40
+ * 1. It NEVER throws. GitHub redelivers anything that is not a 2xx, so an
41
+ * exception escaping handleEvent is a redelivery storm of the exact
42
+ * payload that just failed — forever, at GitHub's pace, not ours.
43
+ *
44
+ * 2. One delivery does at most one thing. GitHub redelivers on failure and
45
+ * on manual replay, and a second "on it" comment on one request reads as
46
+ * a bug even when the run dedupe holds. The fence is keyed on the
47
+ * DELIVERY id — a constant key would swallow every delivery after the
48
+ * first, which is the far worse failure.
49
+ *
50
+ * 3. A command that was understood always gets a visible answer, with ONE
51
+ * deliberate exception: a command from an account without write access
52
+ * gets a reaction and no comment. Replying there would turn the app into
53
+ * a comment-poster any GitHub account can drive at a repository it
54
+ * cannot write to.
55
+ *
56
+ * 4. The branch a run works on is captured HERE, from the pull request
57
+ * lookup, at trigger time — and only when it is a branch this
58
+ * installation can actually reach. A force-push between the comment and
59
+ * the worker claiming the run must not be able to redirect it, and a
60
+ * fork's branch must not be pinned at all, because it does not exist in
61
+ * the repository the run will clone.
62
+ *
63
+ * Everything the handler collaborates with is spied: no Redis, no Postgres, no
64
+ * GitHub. What is deliberately NOT mocked is the pure logic the tree depends
65
+ * on — the command parser, the surface rules, the task-type mapping, the fork
66
+ * check and the reply composer — because those decisions are the behaviour
67
+ * under test, not a dependency of it.
68
+ * ---------------------------------------------------------------------------
69
+ */
70
+
71
+ const ORGANIZATION_NAME: string = "acme";
72
+ const REPOSITORY_NAME: string = "checkout";
73
+ const REPOSITORY_FULL_NAME: string = `${ORGANIZATION_NAME}/${REPOSITORY_NAME}`;
74
+ const INSTALLATION_ID_NUMBER: number = 42424242;
75
+ const INSTALLATION_ID: string = "42424242";
76
+ const SENDER_LOGIN: string = "octo-dev";
77
+ const APP_SLUG: string = "oneuptime";
78
+ const BOT_LOGIN: string = "oneuptime[bot]";
79
+ const PROJECT_NAME: string = "Checkout Platform";
80
+ const DELIVERY_ID: string = "6a8f0e10-0000-11ef-9c1a-0242ac120002";
81
+ const ACK_COMMENT_ID: number = 5150;
82
+ const ISSUE_NUMBER: number = 12;
83
+ const PULL_REQUEST_NUMBER: number = 42;
84
+ const TRIGGER_COMMENT_ID: number = 987654;
85
+ const HEAD_REF_NAME: string = "feature/retry-backoff";
86
+
87
+ describe("GitHubWebhookHandler", () => {
88
+ let codeRepositoryId: ObjectID;
89
+ let projectId: ObjectID;
90
+ let aiRunId: ObjectID;
91
+
92
+ let setStringIfNotExistsSpy: jest.SpyInstance;
93
+ let getAppSlugSpy: jest.SpyInstance;
94
+ let authorizeSpy: jest.SpyInstance;
95
+ let getPullRequestDetailsSpy: jest.SpyInstance;
96
+ let createIssueCommentSpy: jest.SpyInstance;
97
+ let addReactionToCommentSpy: jest.SpyInstance;
98
+ let addReactionToIssueSpy: jest.SpyInstance;
99
+ let enqueueRunSpy: jest.SpyInstance;
100
+ let findRunsSpy: jest.SpyInstance;
101
+ let cancelRunsSpy: jest.SpyInstance;
102
+ let recordAcknowledgementSpy: jest.SpyInstance;
103
+ let acknowledgeSpy: jest.SpyInstance;
104
+
105
+ // ---------------------------------------------------------------- fixtures
106
+
107
+ function connectedRepository(triggerLabel?: string): CodeRepository {
108
+ const codeRepository: CodeRepository = new CodeRepository();
109
+ codeRepository.id = codeRepositoryId;
110
+ codeRepository.projectId = projectId;
111
+ codeRepository.organizationName = ORGANIZATION_NAME;
112
+ codeRepository.repositoryName = REPOSITORY_NAME;
113
+
114
+ if (triggerLabel) {
115
+ codeRepository.gitHubTriggerLabel = triggerLabel;
116
+ }
117
+
118
+ return codeRepository;
119
+ }
120
+
121
+ function allowedAuthorization(
122
+ triggerLabel?: string,
123
+ ): GitHubAuthorizationResult {
124
+ return {
125
+ outcome: GitHubAuthorizationOutcome.Allowed,
126
+ codeRepository: connectedRepository(triggerLabel),
127
+ projectName: PROJECT_NAME,
128
+ };
129
+ }
130
+
131
+ function queuedRun(): AIRun {
132
+ const run: AIRun = new AIRun();
133
+ run.id = aiRunId;
134
+ run.status = AIRunStatus.Queued;
135
+ return run;
136
+ }
137
+
138
+ // A live run as "status" reads it back out of the database.
139
+ function liveRun(commandType: GitHubCommandType): AIRun {
140
+ const run: AIRun = new AIRun();
141
+ run.id = ObjectID.generate();
142
+ run.status = AIRunStatus.Queued;
143
+ run.taskContext = {
144
+ github: {
145
+ codeRepositoryId: codeRepositoryId.toString(),
146
+ installationId: INSTALLATION_ID,
147
+ organizationName: ORGANIZATION_NAME,
148
+ repositoryName: REPOSITORY_NAME,
149
+ commandType: commandType,
150
+ instruction: "",
151
+ pullRequestNumber: PULL_REQUEST_NUMBER,
152
+ },
153
+ };
154
+ return run;
155
+ }
156
+
157
+ function pullRequestDetails(data: {
158
+ state?: string;
159
+ headRefName?: string;
160
+ // Explicitly null models a deleted fork, which GitHub really does send.
161
+ headRepositoryFullName?: string | null;
162
+ pullRequestNumber?: number;
163
+ }): GitHubPullRequestDetails {
164
+ const conversationNumber: number =
165
+ data.pullRequestNumber ?? PULL_REQUEST_NUMBER;
166
+
167
+ return {
168
+ pullRequestNumber: conversationNumber,
169
+ title: "fix: back off the retry loop",
170
+ body: "Retries hammer the upstream on a 429.",
171
+ state: data.state || "open",
172
+ htmlUrl: `https://github.com/${REPOSITORY_FULL_NAME}/pull/${conversationNumber}`,
173
+ authorLogin: SENDER_LOGIN,
174
+ headRefName: data.headRefName || HEAD_REF_NAME,
175
+ headSha: "0f1e2d3c4b5a69788796a5b4c3d2e1f009887766",
176
+ baseRefName: "main",
177
+ headRepositoryFullName:
178
+ data.headRepositoryFullName === undefined
179
+ ? REPOSITORY_FULL_NAME
180
+ : data.headRepositoryFullName,
181
+ isDraft: false,
182
+ isMerged: data.state === "closed",
183
+ changedFilesCount: 3,
184
+ additions: 41,
185
+ deletions: 7,
186
+ };
187
+ }
188
+
189
+ // ---------------------------------------------------------------- payloads
190
+
191
+ // The three keys every webhook carries and resolveContext refuses without.
192
+ function webhookEnvelope(): JSONObject {
193
+ return {
194
+ repository: { full_name: REPOSITORY_FULL_NAME },
195
+ installation: { id: INSTALLATION_ID_NUMBER },
196
+ sender: { login: SENDER_LOGIN, type: "User" },
197
+ };
198
+ }
199
+
200
+ // `issue_comment` on a plain issue: no `pull_request` key anywhere.
201
+ function issueCommentPayload(data: {
202
+ body: string;
203
+ action?: string;
204
+ issueNumber?: number;
205
+ commentId?: number;
206
+ }): JSONObject {
207
+ return {
208
+ ...webhookEnvelope(),
209
+ action: data.action || "created",
210
+ issue: {
211
+ number: data.issueNumber ?? ISSUE_NUMBER,
212
+ title: "Checkout throws on an empty cart",
213
+ },
214
+ comment: {
215
+ id: data.commentId ?? TRIGGER_COMMENT_ID,
216
+ body: data.body,
217
+ user: { login: SENDER_LOGIN, type: "User" },
218
+ },
219
+ };
220
+ }
221
+
222
+ /*
223
+ * `issue_comment` on a PULL REQUEST. GitHub sends the same event name; the
224
+ * only discriminator is the `pull_request` key inside `issue`.
225
+ */
226
+ function pullRequestCommentPayload(data: {
227
+ body: string;
228
+ action?: string;
229
+ pullRequestNumber?: number;
230
+ commentId?: number;
231
+ }): JSONObject {
232
+ const conversationNumber: number =
233
+ data.pullRequestNumber ?? PULL_REQUEST_NUMBER;
234
+
235
+ return {
236
+ ...webhookEnvelope(),
237
+ action: data.action || "created",
238
+ issue: {
239
+ number: conversationNumber,
240
+ title: "fix: back off the retry loop",
241
+ pull_request: {
242
+ url: `https://api.github.com/repos/${REPOSITORY_FULL_NAME}/pulls/${conversationNumber}`,
243
+ },
244
+ },
245
+ comment: {
246
+ id: data.commentId ?? TRIGGER_COMMENT_ID,
247
+ body: data.body,
248
+ user: { login: SENDER_LOGIN, type: "User" },
249
+ },
250
+ };
251
+ }
252
+
253
+ // `pull_request_review`: the body lives under `review`, not `comment`.
254
+ function reviewSubmittedPayload(data: {
255
+ body: string;
256
+ reviewId?: number;
257
+ }): JSONObject {
258
+ return {
259
+ ...webhookEnvelope(),
260
+ action: "submitted",
261
+ pull_request: {
262
+ number: PULL_REQUEST_NUMBER,
263
+ title: "fix: back off the retry loop",
264
+ },
265
+ review: {
266
+ id: data.reviewId ?? 314159,
267
+ body: data.body,
268
+ user: { login: SENDER_LOGIN, type: "User" },
269
+ },
270
+ };
271
+ }
272
+
273
+ function issuesLabeledPayload(data: {
274
+ labelName: string;
275
+ issueNumber?: number;
276
+ isReallyAPullRequest?: boolean;
277
+ }): JSONObject {
278
+ const issue: JSONObject = {
279
+ number: data.issueNumber ?? ISSUE_NUMBER,
280
+ title: "Checkout throws on an empty cart",
281
+ };
282
+
283
+ if (data.isReallyAPullRequest) {
284
+ issue["pull_request"] = {
285
+ url: `https://api.github.com/repos/${REPOSITORY_FULL_NAME}/pulls/${data.issueNumber ?? ISSUE_NUMBER}`,
286
+ };
287
+ }
288
+
289
+ return {
290
+ ...webhookEnvelope(),
291
+ action: "labeled",
292
+ issue: issue,
293
+ label: { name: data.labelName },
294
+ };
295
+ }
296
+
297
+ function issuesAssignedPayload(data: {
298
+ assigneeLogin?: string;
299
+ issueNumber?: number;
300
+ }): JSONObject {
301
+ const payload: JSONObject = {
302
+ ...webhookEnvelope(),
303
+ action: "assigned",
304
+ issue: {
305
+ number: data.issueNumber ?? ISSUE_NUMBER,
306
+ title: "Checkout throws on an empty cart",
307
+ },
308
+ };
309
+
310
+ if (data.assigneeLogin) {
311
+ payload["assignee"] = { login: data.assigneeLogin };
312
+ }
313
+
314
+ return payload;
315
+ }
316
+
317
+ function reviewRequestedPayload(data: {
318
+ requestedReviewerLogin?: string;
319
+ pullRequestNumber?: number;
320
+ action?: string;
321
+ }): JSONObject {
322
+ const payload: JSONObject = {
323
+ ...webhookEnvelope(),
324
+ action: data.action || "review_requested",
325
+ pull_request: {
326
+ number: data.pullRequestNumber ?? PULL_REQUEST_NUMBER,
327
+ title: "fix: back off the retry loop",
328
+ },
329
+ };
330
+
331
+ if (data.requestedReviewerLogin) {
332
+ payload["requested_reviewer"] = { login: data.requestedReviewerLogin };
333
+ }
334
+
335
+ return payload;
336
+ }
337
+
338
+ // ----------------------------------------------------------------- helpers
339
+
340
+ async function handle(data: {
341
+ event: string;
342
+ payload: JSONObject;
343
+ deliveryId?: string;
344
+ }): Promise<GitHubWebhookHandlingResult> {
345
+ return GitHubWebhookHandler.handleEvent({
346
+ event: data.event,
347
+ deliveryId: data.deliveryId ?? DELIVERY_ID,
348
+ payload: data.payload,
349
+ });
350
+ }
351
+
352
+ interface EnqueueArguments {
353
+ projectId: ObjectID;
354
+ taskType: CodeFixTaskType;
355
+ github: GitHubTaskContext;
356
+ }
357
+
358
+ function nthCallArgument(spy: jest.SpyInstance, callNumber: number): unknown {
359
+ const call: Array<unknown> | undefined = spy.mock.calls[callNumber - 1] as
360
+ | Array<unknown>
361
+ | undefined;
362
+
363
+ if (!call || call[0] === undefined) {
364
+ throw new Error(`Expected at least ${callNumber} call(s), got none.`);
365
+ }
366
+
367
+ return call[0];
368
+ }
369
+
370
+ function enqueueArguments(callNumber: number): EnqueueArguments {
371
+ return nthCallArgument(enqueueRunSpy, callNumber) as EnqueueArguments;
372
+ }
373
+
374
+ function enqueuedContext(callNumber: number): GitHubTaskContext {
375
+ return enqueueArguments(callNumber).github;
376
+ }
377
+
378
+ interface CommentArguments {
379
+ installationId: string;
380
+ organizationName: string;
381
+ repositoryName: string;
382
+ issueNumber: number;
383
+ body: string;
384
+ }
385
+
386
+ function commentArguments(callNumber: number): CommentArguments {
387
+ return nthCallArgument(
388
+ createIssueCommentSpy,
389
+ callNumber,
390
+ ) as CommentArguments;
391
+ }
392
+
393
+ interface ReactionArguments {
394
+ installationId: string;
395
+ organizationName: string;
396
+ repositoryName: string;
397
+ commentId: number;
398
+ reaction: GitHubReaction;
399
+ }
400
+
401
+ function reactionArguments(callNumber: number): ReactionArguments {
402
+ return nthCallArgument(
403
+ addReactionToCommentSpy,
404
+ callNumber,
405
+ ) as ReactionArguments;
406
+ }
407
+
408
+ // Nothing was started, and nothing was written into somebody's repository.
409
+ function expectNoRunAndNoWrites(): void {
410
+ expect(enqueueRunSpy).not.toHaveBeenCalled();
411
+ expect(acknowledgeSpy).not.toHaveBeenCalled();
412
+ expect(recordAcknowledgementSpy).not.toHaveBeenCalled();
413
+ expect(cancelRunsSpy).not.toHaveBeenCalled();
414
+ expect(createIssueCommentSpy).not.toHaveBeenCalled();
415
+ expect(addReactionToCommentSpy).not.toHaveBeenCalled();
416
+ expect(addReactionToIssueSpy).not.toHaveBeenCalled();
417
+ }
418
+
419
+ beforeEach(() => {
420
+ codeRepositoryId = ObjectID.generate();
421
+ projectId = ObjectID.generate();
422
+ aiRunId = ObjectID.generate();
423
+
424
+ /*
425
+ * The handler logs on every branch, including the ones this file drives on
426
+ * purpose. Silenced rather than mocked away: restoreAllMocks puts the real
427
+ * logger back for anything else in the worker.
428
+ */
429
+ jest.spyOn(logger, "error").mockImplementation((): void => {});
430
+ jest.spyOn(logger, "warn").mockImplementation((): void => {});
431
+ jest.spyOn(logger, "info").mockImplementation((): void => {});
432
+ jest.spyOn(logger, "debug").mockImplementation((): void => {});
433
+
434
+ setStringIfNotExistsSpy = jest.spyOn(GlobalCache, "setStringIfNotExists");
435
+ // true == "this delivery id was claimed by us", i.e. not a redelivery.
436
+ setStringIfNotExistsSpy.mockResolvedValue(true);
437
+
438
+ getAppSlugSpy = jest.spyOn(GitHubConversation, "getAppSlug");
439
+ getAppSlugSpy.mockResolvedValue(APP_SLUG);
440
+
441
+ authorizeSpy = jest.spyOn(GitHubCommandAuthorizer, "authorize");
442
+ authorizeSpy.mockResolvedValue(allowedAuthorization());
443
+
444
+ getPullRequestDetailsSpy = jest.spyOn(
445
+ GitHubConversation,
446
+ "getPullRequestDetails",
447
+ );
448
+ getPullRequestDetailsSpy.mockResolvedValue(pullRequestDetails({}));
449
+
450
+ createIssueCommentSpy = jest.spyOn(
451
+ GitHubConversation,
452
+ "createIssueComment",
453
+ );
454
+ createIssueCommentSpy.mockResolvedValue({
455
+ commentId: 24680,
456
+ htmlUrl: `https://github.com/${REPOSITORY_FULL_NAME}/issues/1#issuecomment-24680`,
457
+ body: "",
458
+ authorLogin: BOT_LOGIN,
459
+ isBot: true,
460
+ createdAt: "2026-09-10T09:00:00Z",
461
+ });
462
+
463
+ addReactionToCommentSpy = jest.spyOn(
464
+ GitHubConversation,
465
+ "addReactionToComment",
466
+ );
467
+ addReactionToCommentSpy.mockResolvedValue(true);
468
+
469
+ addReactionToIssueSpy = jest.spyOn(
470
+ GitHubConversation,
471
+ "addReactionToIssue",
472
+ );
473
+ addReactionToIssueSpy.mockResolvedValue(true);
474
+
475
+ enqueueRunSpy = jest.spyOn(
476
+ GitHubAgentTaskTrigger,
477
+ "enqueueGitHubCodeFixRun",
478
+ );
479
+ enqueueRunSpy.mockResolvedValue(queuedRun());
480
+
481
+ findRunsSpy = jest.spyOn(
482
+ GitHubAgentTaskTrigger,
483
+ "findNonTerminalRunsForConversation",
484
+ );
485
+ findRunsSpy.mockResolvedValue([]);
486
+
487
+ cancelRunsSpy = jest.spyOn(
488
+ GitHubAgentTaskTrigger,
489
+ "cancelRunsForConversation",
490
+ );
491
+ cancelRunsSpy.mockResolvedValue(0);
492
+
493
+ recordAcknowledgementSpy = jest.spyOn(
494
+ GitHubAgentTaskTrigger,
495
+ "recordAcknowledgementComment",
496
+ );
497
+ recordAcknowledgementSpy.mockResolvedValue(undefined);
498
+
499
+ acknowledgeSpy = jest.spyOn(GitHubRunReply, "acknowledge");
500
+ acknowledgeSpy.mockResolvedValue(ACK_COMMENT_ID);
501
+ });
502
+
503
+ afterEach(() => {
504
+ jest.restoreAllMocks();
505
+ });
506
+
507
+ /*
508
+ * The app is subscribed to more events than it acts on, and the ones it
509
+ * ignores are the high-volume ones. Touching the dedupe cache or the
510
+ * authorizer for a push would put a Redis round trip and a database read on
511
+ * every commit in every connected repository.
512
+ */
513
+ describe("events it does not act on", () => {
514
+ test.each(["push", "installation", "installation_repositories", "star"])(
515
+ "ignores %s without touching the cache, the authorizer or anything else",
516
+ async (event: string) => {
517
+ const result: GitHubWebhookHandlingResult = await handle({
518
+ event: event,
519
+ payload: issueCommentPayload({ body: `@${APP_SLUG} implement this` }),
520
+ });
521
+
522
+ expect(result.handled).toBe(false);
523
+ expect(result.outcome).toBe("event-not-interactive");
524
+ expect(setStringIfNotExistsSpy).not.toHaveBeenCalled();
525
+ expect(authorizeSpy).not.toHaveBeenCalled();
526
+ expectNoRunAndNoWrites();
527
+ },
528
+ );
529
+
530
+ test("ignores a delivery with no event header at all", async () => {
531
+ const result: GitHubWebhookHandlingResult =
532
+ await GitHubWebhookHandler.handleEvent({
533
+ event: undefined,
534
+ deliveryId: DELIVERY_ID,
535
+ payload: issueCommentPayload({ body: `@${APP_SLUG} implement this` }),
536
+ });
537
+
538
+ expect(result.handled).toBe(false);
539
+ expect(result.outcome).toBe("event-not-interactive");
540
+ expectNoRunAndNoWrites();
541
+ });
542
+
543
+ test("ignores an actionable event whose action is not one (a deleted comment)", async () => {
544
+ const result: GitHubWebhookHandlingResult = await handle({
545
+ event: "issue_comment",
546
+ payload: issueCommentPayload({
547
+ body: `@${APP_SLUG} implement this`,
548
+ action: "deleted",
549
+ }),
550
+ });
551
+
552
+ expect(result.handled).toBe(false);
553
+ expect(result.outcome).toBe("action-not-actionable");
554
+ expect(authorizeSpy).not.toHaveBeenCalled();
555
+ expectNoRunAndNoWrites();
556
+ });
557
+
558
+ /*
559
+ * People really do go back and add the mention to a comment they already
560
+ * posted, so `edited` has to stay actionable.
561
+ */
562
+ test("acts on an edited comment, because adding the mention later is a real way to ask", async () => {
563
+ const result: GitHubWebhookHandlingResult = await handle({
564
+ event: "issue_comment",
565
+ payload: issueCommentPayload({
566
+ body: `@${APP_SLUG} implement this`,
567
+ action: "edited",
568
+ }),
569
+ });
570
+
571
+ expect(result.handled).toBe(true);
572
+ expect(result.outcome).toBe("queued");
573
+ });
574
+ });
575
+
576
+ describe("delivery de-duplication", () => {
577
+ test("a redelivered delivery id is dropped before the authorizer is consulted", async () => {
578
+ // false == the key already existed, i.e. we have seen this delivery.
579
+ setStringIfNotExistsSpy.mockResolvedValue(false);
580
+
581
+ const result: GitHubWebhookHandlingResult = await handle({
582
+ event: "issue_comment",
583
+ payload: issueCommentPayload({ body: `@${APP_SLUG} implement this` }),
584
+ });
585
+
586
+ expect(result.handled).toBe(false);
587
+ expect(result.outcome).toBe("duplicate-delivery");
588
+ expect(authorizeSpy).not.toHaveBeenCalled();
589
+ expectNoRunAndNoWrites();
590
+ });
591
+
592
+ /*
593
+ * The fence must be keyed on the DELIVERY id. A constant key would look
594
+ * like it worked in a single-delivery test and then silently swallow every
595
+ * webhook after the first one for the whole TTL.
596
+ */
597
+ test("fences on the delivery id itself, with a bounded expiry", async () => {
598
+ await handle({
599
+ event: "issue_comment",
600
+ payload: issueCommentPayload({ body: `@${APP_SLUG} implement this` }),
601
+ deliveryId: "delivery-abc",
602
+ });
603
+
604
+ expect(setStringIfNotExistsSpy).toHaveBeenCalledTimes(1);
605
+
606
+ const call: Array<unknown> = setStringIfNotExistsSpy.mock
607
+ .calls[0] as Array<unknown>;
608
+
609
+ expect(call[1]).toBe("delivery-abc");
610
+ expect(
611
+ (call[3] as { expiresInSeconds?: number } | undefined)
612
+ ?.expiresInSeconds,
613
+ ).toBeGreaterThan(0);
614
+ });
615
+
616
+ test("two different delivery ids are two different requests", async () => {
617
+ await handle({
618
+ event: "issue_comment",
619
+ payload: issueCommentPayload({ body: `@${APP_SLUG} implement this` }),
620
+ deliveryId: "delivery-one",
621
+ });
622
+ await handle({
623
+ event: "issue_comment",
624
+ payload: issueCommentPayload({
625
+ body: `@${APP_SLUG} implement this`,
626
+ issueNumber: 13,
627
+ }),
628
+ deliveryId: "delivery-two",
629
+ });
630
+
631
+ expect(enqueueRunSpy).toHaveBeenCalledTimes(2);
632
+ });
633
+
634
+ /*
635
+ * No delivery id means nothing to fence ON. Treating that as "seen" would
636
+ * silently discard the delivery instead of doing the work.
637
+ */
638
+ test("a missing delivery id is not a duplicate, and is not fenced", async () => {
639
+ const result: GitHubWebhookHandlingResult =
640
+ await GitHubWebhookHandler.handleEvent({
641
+ event: "issue_comment",
642
+ deliveryId: undefined,
643
+ payload: issueCommentPayload({ body: `@${APP_SLUG} implement this` }),
644
+ });
645
+
646
+ expect(setStringIfNotExistsSpy).not.toHaveBeenCalled();
647
+ expect(result.handled).toBe(true);
648
+ expect(result.outcome).toBe("queued");
649
+ });
650
+
651
+ test("an empty-string delivery id is not a duplicate either", async () => {
652
+ const result: GitHubWebhookHandlingResult =
653
+ await GitHubWebhookHandler.handleEvent({
654
+ event: "issue_comment",
655
+ deliveryId: "",
656
+ payload: issueCommentPayload({ body: `@${APP_SLUG} implement this` }),
657
+ });
658
+
659
+ expect(setStringIfNotExistsSpy).not.toHaveBeenCalled();
660
+ expect(result.outcome).toBe("queued");
661
+ });
662
+
663
+ /*
664
+ * A Redis outage must not stop the app working. Failing closed here would
665
+ * take the whole integration down with the cache — a much larger outage
666
+ * than the duplicate comment the fence exists to avoid.
667
+ */
668
+ test("a throwing cache is not treated as a duplicate", async () => {
669
+ setStringIfNotExistsSpy.mockRejectedValue(
670
+ new Error("Cache is not connected"),
671
+ );
672
+
673
+ const result: GitHubWebhookHandlingResult = await handle({
674
+ event: "issue_comment",
675
+ payload: issueCommentPayload({ body: `@${APP_SLUG} implement this` }),
676
+ });
677
+
678
+ expect(result.handled).toBe(true);
679
+ expect(result.outcome).toBe("queued");
680
+ expect(enqueueRunSpy).toHaveBeenCalledTimes(1);
681
+ });
682
+
683
+ test("isDuplicateDelivery answers false for a cache that throws", async () => {
684
+ setStringIfNotExistsSpy.mockRejectedValue(
685
+ new Error("Cache is not connected"),
686
+ );
687
+
688
+ await expect(
689
+ GitHubWebhookHandler.isDuplicateDelivery("delivery-abc"),
690
+ ).resolves.toBe(false);
691
+ });
692
+ });
693
+
694
+ /*
695
+ * GitHub retries any delivery that did not get a 2xx. An exception escaping
696
+ * handleEvent therefore does not fail once — it fails on a schedule we do
697
+ * not control, replaying the same broken payload.
698
+ */
699
+ describe("handleEvent never throws", () => {
700
+ test("a rejecting authorizer becomes handled:false / error, not an exception", async () => {
701
+ authorizeSpy.mockRejectedValue(new Error("Postgres connection lost"));
702
+
703
+ const result: GitHubWebhookHandlingResult = await handle({
704
+ event: "issue_comment",
705
+ payload: issueCommentPayload({ body: `@${APP_SLUG} implement this` }),
706
+ });
707
+
708
+ expect(result.handled).toBe(false);
709
+ expect(result.outcome).toBe("error");
710
+ expectNoRunAndNoWrites();
711
+ });
712
+
713
+ test("a rejecting pull request lookup does not escape either", async () => {
714
+ getPullRequestDetailsSpy.mockRejectedValue(new Error("502 from GitHub"));
715
+
716
+ const result: GitHubWebhookHandlingResult = await handle({
717
+ event: "issue_comment",
718
+ payload: pullRequestCommentPayload({ body: `@${APP_SLUG} review` }),
719
+ });
720
+
721
+ expect(result.handled).toBe(false);
722
+ expect(result.outcome).toBe("error");
723
+ expect(enqueueRunSpy).not.toHaveBeenCalled();
724
+ });
725
+
726
+ test("a rejecting app-slug lookup does not escape either", async () => {
727
+ getAppSlugSpy.mockRejectedValue(new Error("GitHub App JWT is invalid"));
728
+
729
+ const result: GitHubWebhookHandlingResult = await handle({
730
+ event: "issue_comment",
731
+ payload: issueCommentPayload({ body: `@${APP_SLUG} implement this` }),
732
+ });
733
+
734
+ expect(result.handled).toBe(false);
735
+ expect(result.outcome).toBe("error");
736
+ });
737
+
738
+ /*
739
+ * The acknowledgement happens AFTER the run is queued, so a throw there is
740
+ * the one place an exception could undo work that already succeeded. It
741
+ * must still be swallowed: the run stands, and the redelivery GitHub sends
742
+ * is caught by the delivery fence rather than queueing a second run.
743
+ */
744
+ test("an acknowledgement that throws does not escape, and does not un-queue the run", async () => {
745
+ acknowledgeSpy.mockRejectedValue(new Error("503 from GitHub"));
746
+
747
+ const result: GitHubWebhookHandlingResult = await handle({
748
+ event: "issue_comment",
749
+ payload: issueCommentPayload({ body: `@${APP_SLUG} implement this` }),
750
+ });
751
+
752
+ expect(enqueueRunSpy).toHaveBeenCalledTimes(1);
753
+ expect(result.handled).toBe(false);
754
+ expect(result.outcome).toBe("error");
755
+ });
756
+ });
757
+
758
+ describe("a mention on an issue", () => {
759
+ test("queues a GitHubIssueFix run", async () => {
760
+ const result: GitHubWebhookHandlingResult = await handle({
761
+ event: "issue_comment",
762
+ payload: issueCommentPayload({
763
+ body: `@${APP_SLUG} implement this — the cart guard is missing`,
764
+ }),
765
+ });
766
+
767
+ expect(result.handled).toBe(true);
768
+ expect(result.outcome).toBe("queued");
769
+ expect(result.aiRunId?.toString()).toBe(aiRunId.toString());
770
+ expect(enqueueArguments(1).taskType).toBe(CodeFixTaskType.GitHubIssueFix);
771
+ expect(enqueueArguments(1).projectId.toString()).toBe(
772
+ projectId.toString(),
773
+ );
774
+ });
775
+
776
+ /*
777
+ * issueNumber and pullRequestNumber are mutually exclusive and they pick
778
+ * the recipe. Setting the wrong one sends an issue to the pull request
779
+ * revision agent, which then tries to push to a branch that does not exist.
780
+ */
781
+ test("carries the ISSUE number, and no pull request number", async () => {
782
+ await handle({
783
+ event: "issue_comment",
784
+ payload: issueCommentPayload({
785
+ body: `@${APP_SLUG} implement this`,
786
+ issueNumber: 12,
787
+ }),
788
+ });
789
+
790
+ expect(enqueuedContext(1).issueNumber).toBe(12);
791
+ expect(enqueuedContext(1).pullRequestNumber).toBeUndefined();
792
+ expect(enqueuedContext(1).pullRequestHeadRefName).toBeUndefined();
793
+ // An issue has no head branch and no fork question to answer.
794
+ expect(enqueuedContext(1).isPullRequestFromFork).toBeUndefined();
795
+ });
796
+
797
+ test("does not look up a pull request for an issue command", async () => {
798
+ await handle({
799
+ event: "issue_comment",
800
+ payload: issueCommentPayload({ body: `@${APP_SLUG} implement this` }),
801
+ });
802
+
803
+ expect(getPullRequestDetailsSpy).not.toHaveBeenCalled();
804
+ });
805
+
806
+ /*
807
+ * Everything the worker needs comes from the SIGNED payload, captured now.
808
+ * The worker cannot see the webhook, so anything missing here is gone.
809
+ */
810
+ test("carries the installation, the org/repo and the OneUptime repository row", async () => {
811
+ await handle({
812
+ event: "issue_comment",
813
+ payload: issueCommentPayload({ body: `@${APP_SLUG} implement this` }),
814
+ });
815
+
816
+ const github: GitHubTaskContext = enqueuedContext(1);
817
+
818
+ expect(github.installationId).toBe(INSTALLATION_ID);
819
+ expect(github.organizationName).toBe(ORGANIZATION_NAME);
820
+ expect(github.repositoryName).toBe(REPOSITORY_NAME);
821
+ expect(github.codeRepositoryId).toBe(codeRepositoryId.toString());
822
+ expect(github.commandType).toBe(GitHubCommandType.Implement);
823
+ });
824
+
825
+ test("carries the triggering comment id and the sender's login", async () => {
826
+ await handle({
827
+ event: "issue_comment",
828
+ payload: issueCommentPayload({
829
+ body: `@${APP_SLUG} implement this`,
830
+ commentId: 777001,
831
+ }),
832
+ });
833
+
834
+ expect(enqueuedContext(1).triggerCommentId).toBe(777001);
835
+ expect(enqueuedContext(1).triggeredByLogin).toBe(SENDER_LOGIN);
836
+ expect(enqueuedContext(1).webhookDeliveryId).toBe(DELIVERY_ID);
837
+ });
838
+
839
+ /*
840
+ * The instruction is untrusted text that ends up quoted into a prompt. It
841
+ * must survive verbatim — a handler that normalizes or truncates it here
842
+ * changes what the user asked for before anyone can read it back.
843
+ */
844
+ test("carries a unicode instruction through byte for byte", async () => {
845
+ const instruction: string =
846
+ "implement this — 空のカートを守って 🙏 (and keep the public API)";
847
+
848
+ await handle({
849
+ event: "issue_comment",
850
+ payload: issueCommentPayload({ body: `@${APP_SLUG} ${instruction}` }),
851
+ });
852
+
853
+ expect(enqueuedContext(1).instruction).toBe(instruction);
854
+ });
855
+
856
+ test("acknowledges the run and records the acknowledgement comment id", async () => {
857
+ await handle({
858
+ event: "issue_comment",
859
+ payload: issueCommentPayload({ body: `@${APP_SLUG} implement this` }),
860
+ });
861
+
862
+ expect(acknowledgeSpy).toHaveBeenCalledTimes(1);
863
+
864
+ const acknowledgement: {
865
+ aiRunId: ObjectID;
866
+ projectId: ObjectID;
867
+ projectName: string | undefined;
868
+ github: GitHubTaskContext;
869
+ } = nthCallArgument(acknowledgeSpy, 1) as {
870
+ aiRunId: ObjectID;
871
+ projectId: ObjectID;
872
+ projectName: string | undefined;
873
+ github: GitHubTaskContext;
874
+ };
875
+
876
+ expect(acknowledgement.aiRunId.toString()).toBe(aiRunId.toString());
877
+ expect(acknowledgement.projectName).toBe(PROJECT_NAME);
878
+
879
+ const recorded: {
880
+ aiRunId: ObjectID;
881
+ acknowledgementCommentId: number;
882
+ } = nthCallArgument(recordAcknowledgementSpy, 1) as {
883
+ aiRunId: ObjectID;
884
+ acknowledgementCommentId: number;
885
+ };
886
+
887
+ expect(recorded.acknowledgementCommentId).toBe(ACK_COMMENT_ID);
888
+ expect(recorded.aiRunId.toString()).toBe(aiRunId.toString());
889
+ });
890
+
891
+ /*
892
+ * No comment id means there is no comment to edit later. Recording a null
893
+ * would make the finishing sweep edit "comment null" instead of posting.
894
+ */
895
+ test("records nothing when GitHub did not give back a comment id", async () => {
896
+ acknowledgeSpy.mockResolvedValue(null);
897
+
898
+ const result: GitHubWebhookHandlingResult = await handle({
899
+ event: "issue_comment",
900
+ payload: issueCommentPayload({ body: `@${APP_SLUG} implement this` }),
901
+ });
902
+
903
+ expect(recordAcknowledgementSpy).not.toHaveBeenCalled();
904
+ expect(result.outcome).toBe("queued");
905
+ });
906
+
907
+ /*
908
+ * The overwhelmingly common case: this runs on every comment in every
909
+ * connected repository, and almost none of them are for us.
910
+ */
911
+ test("an ordinary comment that never mentions the app does nothing", async () => {
912
+ const result: GitHubWebhookHandlingResult = await handle({
913
+ event: "issue_comment",
914
+ payload: issueCommentPayload({
915
+ body: "I think the cart guard is missing here, will pick it up.",
916
+ }),
917
+ });
918
+
919
+ expect(result.handled).toBe(false);
920
+ expect(result.outcome).toBe("no-mention");
921
+ expectNoRunAndNoWrites();
922
+ });
923
+
924
+ /*
925
+ * GitHub's "Quote reply" button copies the parent comment into a `>`
926
+ * block. Without this, every reply to one of the app's own comments
927
+ * re-triggers it — a comment loop that bills the project until someone
928
+ * notices.
929
+ */
930
+ test("a mention quoted from an earlier comment does not re-trigger a run", async () => {
931
+ const result: GitHubWebhookHandlingResult = await handle({
932
+ event: "issue_comment",
933
+ payload: issueCommentPayload({
934
+ body: `> @${APP_SLUG} implement this\n\nThanks, I will take it from here.`,
935
+ }),
936
+ });
937
+
938
+ expect(result.outcome).toBe("no-mention");
939
+ expectNoRunAndNoWrites();
940
+ });
941
+
942
+ test("a mention of a DIFFERENT app is not a mention of this one", async () => {
943
+ const result: GitHubWebhookHandlingResult = await handle({
944
+ event: "issue_comment",
945
+ payload: issueCommentPayload({
946
+ body: `@${APP_SLUG}-staging implement this`,
947
+ }),
948
+ });
949
+
950
+ expect(result.outcome).toBe("no-mention");
951
+ expectNoRunAndNoWrites();
952
+ });
953
+ });
954
+
955
+ describe("a mention on a pull request", () => {
956
+ test("queues a GitHubPullRequestRevision run for a revise command", async () => {
957
+ const result: GitHubWebhookHandlingResult = await handle({
958
+ event: "issue_comment",
959
+ payload: pullRequestCommentPayload({
960
+ body: `@${APP_SLUG} revise this because the retry loop never backs off`,
961
+ }),
962
+ });
963
+
964
+ expect(result.outcome).toBe("queued");
965
+ expect(enqueueArguments(1).taskType).toBe(
966
+ CodeFixTaskType.GitHubPullRequestRevision,
967
+ );
968
+ expect(enqueuedContext(1).commandType).toBe(GitHubCommandType.Revise);
969
+ });
970
+
971
+ test("carries the PULL REQUEST number, and no issue number", async () => {
972
+ await handle({
973
+ event: "issue_comment",
974
+ payload: pullRequestCommentPayload({
975
+ body: `@${APP_SLUG} revise this`,
976
+ pullRequestNumber: 42,
977
+ }),
978
+ });
979
+
980
+ expect(enqueuedContext(1).pullRequestNumber).toBe(42);
981
+ expect(enqueuedContext(1).issueNumber).toBeUndefined();
982
+ });
983
+
984
+ /*
985
+ * THE force-push guard. The head branch is read once, here, and pinned on
986
+ * the run. If the worker re-derived it when it claimed the task, a push to
987
+ * the pull request in between would silently point the revision at a
988
+ * different branch than the one the human was looking at.
989
+ */
990
+ test("pins the head branch from the pull request lookup, not from the payload", async () => {
991
+ getPullRequestDetailsSpy.mockResolvedValue(
992
+ pullRequestDetails({ headRefName: "hotfix/retry-backoff-v2" }),
993
+ );
994
+
995
+ await handle({
996
+ event: "issue_comment",
997
+ payload: pullRequestCommentPayload({
998
+ body: `@${APP_SLUG} revise this`,
999
+ }),
1000
+ });
1001
+
1002
+ expect(enqueuedContext(1).pullRequestHeadRefName).toBe(
1003
+ "hotfix/retry-backoff-v2",
1004
+ );
1005
+ expect(enqueuedContext(1).isPullRequestFromFork).toBe(false);
1006
+ });
1007
+
1008
+ test("looks the pull request up on the repository the webhook named", async () => {
1009
+ await handle({
1010
+ event: "issue_comment",
1011
+ payload: pullRequestCommentPayload({
1012
+ body: `@${APP_SLUG} revise this`,
1013
+ pullRequestNumber: 42,
1014
+ }),
1015
+ });
1016
+
1017
+ const lookup: {
1018
+ installationId: string;
1019
+ organizationName: string;
1020
+ repositoryName: string;
1021
+ pullRequestNumber: number;
1022
+ } = nthCallArgument(getPullRequestDetailsSpy, 1) as {
1023
+ installationId: string;
1024
+ organizationName: string;
1025
+ repositoryName: string;
1026
+ pullRequestNumber: number;
1027
+ };
1028
+
1029
+ expect(lookup.pullRequestNumber).toBe(42);
1030
+ expect(lookup.installationId).toBe(INSTALLATION_ID);
1031
+ expect(lookup.organizationName).toBe(ORGANIZATION_NAME);
1032
+ expect(lookup.repositoryName).toBe(REPOSITORY_NAME);
1033
+ });
1034
+
1035
+ test("queues a GitHubPullRequestReview run for a review command", async () => {
1036
+ const result: GitHubWebhookHandlingResult = await handle({
1037
+ event: "issue_comment",
1038
+ payload: pullRequestCommentPayload({ body: `@${APP_SLUG} review` }),
1039
+ });
1040
+
1041
+ expect(result.outcome).toBe("queued");
1042
+ expect(enqueueArguments(1).taskType).toBe(
1043
+ CodeFixTaskType.GitHubPullRequestReview,
1044
+ );
1045
+ expect(enqueuedContext(1).commandType).toBe(GitHubCommandType.Review);
1046
+ });
1047
+
1048
+ /*
1049
+ * The review events carry the body under `review`, and there is no `issue`
1050
+ * key at all — the surface has to come from `pull_request`. Reading it
1051
+ * wrong turns a review request into an "implement" on a pull request.
1052
+ */
1053
+ test("reads a submitted review's body, and treats the surface as a pull request", async () => {
1054
+ const result: GitHubWebhookHandlingResult = await handle({
1055
+ event: "pull_request_review",
1056
+ payload: reviewSubmittedPayload({
1057
+ body: `@${APP_SLUG} review`,
1058
+ reviewId: 314159,
1059
+ }),
1060
+ });
1061
+
1062
+ expect(result.outcome).toBe("queued");
1063
+ expect(enqueueArguments(1).taskType).toBe(
1064
+ CodeFixTaskType.GitHubPullRequestReview,
1065
+ );
1066
+ expect(enqueuedContext(1).pullRequestNumber).toBe(PULL_REQUEST_NUMBER);
1067
+ expect(enqueuedContext(1).triggerCommentId).toBe(314159);
1068
+ });
1069
+ });
1070
+
1071
+ describe("commands that cannot be carried out where they were written", () => {
1072
+ /*
1073
+ * A silently dropped command reads as a broken integration. The user gets
1074
+ * told what to do instead — but no run is started and no pull request is
1075
+ * looked up.
1076
+ */
1077
+ test("a review asked for on an ISSUE is answered, not run", async () => {
1078
+ const result: GitHubWebhookHandlingResult = await handle({
1079
+ event: "issue_comment",
1080
+ payload: issueCommentPayload({ body: `@${APP_SLUG} review` }),
1081
+ });
1082
+
1083
+ expect(result.handled).toBe(true);
1084
+ expect(result.outcome).toBe("unsupported-on-surface");
1085
+ expect(enqueueRunSpy).not.toHaveBeenCalled();
1086
+ expect(getPullRequestDetailsSpy).not.toHaveBeenCalled();
1087
+ expect(createIssueCommentSpy).toHaveBeenCalledTimes(1);
1088
+ expect(commentArguments(1).body).toContain(
1089
+ "I can only review pull requests",
1090
+ );
1091
+ expect(commentArguments(1).issueNumber).toBe(ISSUE_NUMBER);
1092
+ });
1093
+
1094
+ test("an implement asked for on a PULL REQUEST is answered, not run", async () => {
1095
+ const result: GitHubWebhookHandlingResult = await handle({
1096
+ event: "issue_comment",
1097
+ payload: pullRequestCommentPayload({
1098
+ body: `@${APP_SLUG} implement this`,
1099
+ }),
1100
+ });
1101
+
1102
+ expect(result.handled).toBe(true);
1103
+ expect(result.outcome).toBe("unsupported-on-surface");
1104
+ expect(enqueueRunSpy).not.toHaveBeenCalled();
1105
+ expect(commentArguments(1).body).toContain("I can only implement issues");
1106
+ });
1107
+
1108
+ /*
1109
+ * The loop guard, checked the only way that actually proves it: feed the
1110
+ * comment this app just posted back through the real parser. Its own
1111
+ * comments arrive as webhooks like anyone else's, so a reply the parser
1112
+ * reads as a command is a two-comment loop that bills the project until
1113
+ * somebody notices. Asserting the absence of an "@" string would be both
1114
+ * weaker (a mention is only a command in a commandable position) and more
1115
+ * brittle (the help text legitimately prints one inside a code fence).
1116
+ */
1117
+ test("the refusal it posts cannot be read back as a command", async () => {
1118
+ await handle({
1119
+ event: "issue_comment",
1120
+ payload: issueCommentPayload({ body: `@${APP_SLUG} review` }),
1121
+ });
1122
+
1123
+ expect(
1124
+ GitHubCommandParser.parse({
1125
+ body: commentArguments(1).body,
1126
+ appSlug: APP_SLUG,
1127
+ surface: GitHubCommandSurface.Issue,
1128
+ }),
1129
+ ).toBeNull();
1130
+ });
1131
+ });
1132
+
1133
+ describe("pull requests it refuses to work on", () => {
1134
+ test("a closed pull request is answered and no run is started", async () => {
1135
+ getPullRequestDetailsSpy.mockResolvedValue(
1136
+ pullRequestDetails({ state: "closed" }),
1137
+ );
1138
+
1139
+ const result: GitHubWebhookHandlingResult = await handle({
1140
+ event: "issue_comment",
1141
+ payload: pullRequestCommentPayload({
1142
+ body: `@${APP_SLUG} revise this`,
1143
+ }),
1144
+ });
1145
+
1146
+ expect(result.handled).toBe(true);
1147
+ expect(result.outcome).toBe("pull-request-not-open");
1148
+ expect(enqueueRunSpy).not.toHaveBeenCalled();
1149
+ expect(createIssueCommentSpy).toHaveBeenCalledTimes(1);
1150
+ expect(commentArguments(1).body).toContain("already closed");
1151
+ expect(commentArguments(1).issueNumber).toBe(PULL_REQUEST_NUMBER);
1152
+ });
1153
+
1154
+ test("a merged pull request is refused for a REVIEW as well", async () => {
1155
+ getPullRequestDetailsSpy.mockResolvedValue(
1156
+ pullRequestDetails({ state: "closed" }),
1157
+ );
1158
+
1159
+ const result: GitHubWebhookHandlingResult = await handle({
1160
+ event: "issue_comment",
1161
+ payload: pullRequestCommentPayload({ body: `@${APP_SLUG} review` }),
1162
+ });
1163
+
1164
+ expect(result.outcome).toBe("pull-request-not-open");
1165
+ expect(enqueueRunSpy).not.toHaveBeenCalled();
1166
+ });
1167
+
1168
+ /*
1169
+ * A fork's branch lives in a repository this installation cannot write to.
1170
+ * Discovering that after a clone and a full agent run spends the budget
1171
+ * and ends in a confusing push error.
1172
+ */
1173
+ test("a revise on a FORK pull request is refused up front", async () => {
1174
+ getPullRequestDetailsSpy.mockResolvedValue(
1175
+ pullRequestDetails({ headRepositoryFullName: "contributor/checkout" }),
1176
+ );
1177
+
1178
+ const result: GitHubWebhookHandlingResult = await handle({
1179
+ event: "issue_comment",
1180
+ payload: pullRequestCommentPayload({
1181
+ body: `@${APP_SLUG} revise this`,
1182
+ }),
1183
+ });
1184
+
1185
+ expect(result.handled).toBe(true);
1186
+ expect(result.outcome).toBe("pull-request-from-fork");
1187
+ expect(enqueueRunSpy).not.toHaveBeenCalled();
1188
+ expect(commentArguments(1).body).toContain("comes from a fork");
1189
+ });
1190
+
1191
+ /*
1192
+ * The other half of the same rule, and the one a naive "forks are unsafe"
1193
+ * fix would break: a review is WRITTEN on the base repository's pull
1194
+ * request, so a fork changes nothing about it.
1195
+ */
1196
+ test("a REVIEW of the same fork pull request is allowed", async () => {
1197
+ getPullRequestDetailsSpy.mockResolvedValue(
1198
+ pullRequestDetails({ headRepositoryFullName: "contributor/checkout" }),
1199
+ );
1200
+
1201
+ const result: GitHubWebhookHandlingResult = await handle({
1202
+ event: "issue_comment",
1203
+ payload: pullRequestCommentPayload({ body: `@${APP_SLUG} review` }),
1204
+ });
1205
+
1206
+ expect(result.outcome).toBe("queued");
1207
+ expect(enqueueArguments(1).taskType).toBe(
1208
+ CodeFixTaskType.GitHubPullRequestReview,
1209
+ );
1210
+ expect(createIssueCommentSpy).not.toHaveBeenCalled();
1211
+ });
1212
+
1213
+ /*
1214
+ * A fork's head branch does not exist in the repository this installation
1215
+ * can reach, so pinning it would send the run after a ref that is not
1216
+ * there — which a clone answers with the default branch, quietly reviewing
1217
+ * the wrong code. The flag says why it is missing, so the recipe can work
1218
+ * from the base plus the diff instead of guessing.
1219
+ */
1220
+ test("a fork review carries the fork flag and NO head branch to check out", async () => {
1221
+ getPullRequestDetailsSpy.mockResolvedValue(
1222
+ pullRequestDetails({ headRepositoryFullName: "contributor/checkout" }),
1223
+ );
1224
+
1225
+ await handle({
1226
+ event: "issue_comment",
1227
+ payload: pullRequestCommentPayload({ body: `@${APP_SLUG} review` }),
1228
+ });
1229
+
1230
+ expect(enqueuedContext(1).isPullRequestFromFork).toBe(true);
1231
+ expect(enqueuedContext(1).pullRequestHeadRefName).toBeUndefined();
1232
+ });
1233
+
1234
+ // GitHub reports a null head repository for a fork that has been deleted.
1235
+ test("a revise on a pull request whose head repository is gone is refused", async () => {
1236
+ getPullRequestDetailsSpy.mockResolvedValue(
1237
+ pullRequestDetails({ headRepositoryFullName: null }),
1238
+ );
1239
+
1240
+ const result: GitHubWebhookHandlingResult = await handle({
1241
+ event: "issue_comment",
1242
+ payload: pullRequestCommentPayload({
1243
+ body: `@${APP_SLUG} revise this`,
1244
+ }),
1245
+ });
1246
+
1247
+ expect(result.outcome).toBe("pull-request-from-fork");
1248
+ expect(enqueueRunSpy).not.toHaveBeenCalled();
1249
+ });
1250
+
1251
+ /*
1252
+ * GitHub preserves the case of an owner or repository rename. Treating a
1253
+ * case difference as a fork would refuse every revision on a repository
1254
+ * whose owner capitalized their name.
1255
+ */
1256
+ test("a differently-cased head repository is the same repository, not a fork", async () => {
1257
+ getPullRequestDetailsSpy.mockResolvedValue(
1258
+ pullRequestDetails({ headRepositoryFullName: "Acme/Checkout" }),
1259
+ );
1260
+
1261
+ const result: GitHubWebhookHandlingResult = await handle({
1262
+ event: "issue_comment",
1263
+ payload: pullRequestCommentPayload({
1264
+ body: `@${APP_SLUG} revise this`,
1265
+ }),
1266
+ });
1267
+
1268
+ expect(result.outcome).toBe("queued");
1269
+ expect(enqueueArguments(1).taskType).toBe(
1270
+ CodeFixTaskType.GitHubPullRequestRevision,
1271
+ );
1272
+ });
1273
+ });
1274
+
1275
+ /*
1276
+ * help / status / cancel cost no agent run, so none of them may enqueue one
1277
+ * — and all of them must answer, because "why is nothing happening?" is
1278
+ * exactly the question they exist to answer.
1279
+ */
1280
+ describe("conversational commands", () => {
1281
+ test("help replies with the command list and starts nothing", async () => {
1282
+ const result: GitHubWebhookHandlingResult = await handle({
1283
+ event: "issue_comment",
1284
+ payload: issueCommentPayload({ body: `@${APP_SLUG} help` }),
1285
+ });
1286
+
1287
+ expect(result.handled).toBe(true);
1288
+ expect(result.outcome).toBe("help");
1289
+ expect(enqueueRunSpy).not.toHaveBeenCalled();
1290
+ expect(createIssueCommentSpy).toHaveBeenCalledTimes(1);
1291
+ expect(commentArguments(1).body).toContain("Here is what you can ask me");
1292
+ });
1293
+
1294
+ /*
1295
+ * The help text is the one comment this app posts that HAS to print
1296
+ * mentions of itself — it is documentation. They are fenced, and the
1297
+ * parser ignores fenced regions, so the two files agree by construction.
1298
+ * If either side of that arrangement drifts, the app answers its own help
1299
+ * comment, forever.
1300
+ */
1301
+ test("the help text it posts cannot be read back as a command", async () => {
1302
+ await handle({
1303
+ event: "issue_comment",
1304
+ payload: issueCommentPayload({ body: `@${APP_SLUG} help` }),
1305
+ });
1306
+
1307
+ expect(commentArguments(1).body).toContain(`@${APP_SLUG} review`);
1308
+ expect(
1309
+ GitHubCommandParser.parse({
1310
+ body: commentArguments(1).body,
1311
+ appSlug: APP_SLUG,
1312
+ surface: GitHubCommandSurface.Issue,
1313
+ }),
1314
+ ).toBeNull();
1315
+ });
1316
+
1317
+ /*
1318
+ * A bare mention is a request for orientation, not a licence to start work
1319
+ * on whatever the thread happens to be about.
1320
+ */
1321
+ test("a bare mention is help, not an implicit command", async () => {
1322
+ const result: GitHubWebhookHandlingResult = await handle({
1323
+ event: "issue_comment",
1324
+ payload: issueCommentPayload({ body: `Hey @${APP_SLUG}` }),
1325
+ });
1326
+
1327
+ expect(result.outcome).toBe("help");
1328
+ expect(enqueueRunSpy).not.toHaveBeenCalled();
1329
+ });
1330
+
1331
+ test("status reports the live runs for this conversation and starts nothing", async () => {
1332
+ findRunsSpy.mockResolvedValue([liveRun(GitHubCommandType.Revise)]);
1333
+
1334
+ const result: GitHubWebhookHandlingResult = await handle({
1335
+ event: "issue_comment",
1336
+ payload: pullRequestCommentPayload({ body: `@${APP_SLUG} status` }),
1337
+ });
1338
+
1339
+ expect(result.handled).toBe(true);
1340
+ expect(result.outcome).toBe("status");
1341
+ expect(enqueueRunSpy).not.toHaveBeenCalled();
1342
+ expect(commentArguments(1).body).toContain("Revising this pull request");
1343
+
1344
+ const query: {
1345
+ projectId: ObjectID;
1346
+ codeRepositoryId: ObjectID;
1347
+ conversationNumber: number;
1348
+ } = nthCallArgument(findRunsSpy, 1) as {
1349
+ projectId: ObjectID;
1350
+ codeRepositoryId: ObjectID;
1351
+ conversationNumber: number;
1352
+ };
1353
+
1354
+ expect(query.conversationNumber).toBe(PULL_REQUEST_NUMBER);
1355
+ expect(query.codeRepositoryId.toString()).toBe(
1356
+ codeRepositoryId.toString(),
1357
+ );
1358
+ });
1359
+
1360
+ test("status still answers when nothing is running", async () => {
1361
+ findRunsSpy.mockResolvedValue([]);
1362
+
1363
+ const result: GitHubWebhookHandlingResult = await handle({
1364
+ event: "issue_comment",
1365
+ payload: issueCommentPayload({ body: `@${APP_SLUG} status` }),
1366
+ });
1367
+
1368
+ expect(result.outcome).toBe("status");
1369
+ expect(createIssueCommentSpy).toHaveBeenCalledTimes(1);
1370
+ expect(commentArguments(1).body).toContain("not working on anything");
1371
+ });
1372
+
1373
+ test("cancel cancels this conversation's runs and says how many", async () => {
1374
+ cancelRunsSpy.mockResolvedValue(2);
1375
+
1376
+ const result: GitHubWebhookHandlingResult = await handle({
1377
+ event: "issue_comment",
1378
+ payload: pullRequestCommentPayload({ body: `@${APP_SLUG} cancel` }),
1379
+ });
1380
+
1381
+ expect(result.handled).toBe(true);
1382
+ expect(result.outcome).toBe("cancel");
1383
+ expect(enqueueRunSpy).not.toHaveBeenCalled();
1384
+ expect(cancelRunsSpy).toHaveBeenCalledTimes(1);
1385
+
1386
+ const query: {
1387
+ projectId: ObjectID;
1388
+ codeRepositoryId: ObjectID;
1389
+ conversationNumber: number;
1390
+ } = nthCallArgument(cancelRunsSpy, 1) as {
1391
+ projectId: ObjectID;
1392
+ codeRepositoryId: ObjectID;
1393
+ conversationNumber: number;
1394
+ };
1395
+
1396
+ expect(query.conversationNumber).toBe(PULL_REQUEST_NUMBER);
1397
+ expect(query.projectId.toString()).toBe(projectId.toString());
1398
+ expect(commentArguments(1).body).toContain("Cancelled");
1399
+ });
1400
+
1401
+ /*
1402
+ * Cancelling with nothing running is still answered — silence would leave
1403
+ * the user wondering whether the cancel landed.
1404
+ */
1405
+ test("cancel with nothing running still replies", async () => {
1406
+ cancelRunsSpy.mockResolvedValue(0);
1407
+
1408
+ const result: GitHubWebhookHandlingResult = await handle({
1409
+ event: "issue_comment",
1410
+ payload: pullRequestCommentPayload({ body: `@${APP_SLUG} cancel` }),
1411
+ });
1412
+
1413
+ expect(result.outcome).toBe("cancel");
1414
+ expect(commentArguments(1).body).toContain("Nothing of mine is running");
1415
+ });
1416
+
1417
+ // A conversational command never touches the pull request API.
1418
+ test("no conversational command looks up the pull request", async () => {
1419
+ await handle({
1420
+ event: "issue_comment",
1421
+ payload: pullRequestCommentPayload({ body: `@${APP_SLUG} status` }),
1422
+ });
1423
+
1424
+ expect(getPullRequestDetailsSpy).not.toHaveBeenCalled();
1425
+ });
1426
+ });
1427
+
1428
+ describe("an issue handed over with the trigger label", () => {
1429
+ test("the default label starts an Implement run when none is configured", async () => {
1430
+ authorizeSpy.mockResolvedValue(allowedAuthorization());
1431
+
1432
+ const result: GitHubWebhookHandlingResult = await handle({
1433
+ event: "issues",
1434
+ payload: issuesLabeledPayload({ labelName: "oneuptime" }),
1435
+ });
1436
+
1437
+ expect(result.handled).toBe(true);
1438
+ expect(result.outcome).toBe("queued");
1439
+ expect(enqueueArguments(1).taskType).toBe(CodeFixTaskType.GitHubIssueFix);
1440
+ expect(enqueuedContext(1).commandType).toBe(GitHubCommandType.Implement);
1441
+ expect(enqueuedContext(1).issueNumber).toBe(ISSUE_NUMBER);
1442
+ });
1443
+
1444
+ /*
1445
+ * GitHub labels are case-preserving but case-insensitively unique, so
1446
+ * "OneUptime" and "oneuptime" are the SAME label. An exact-match check
1447
+ * would miss half of them, and the feature would look broken at random.
1448
+ */
1449
+ test("matches the configured label case-insensitively and ignoring surrounding space", async () => {
1450
+ authorizeSpy.mockResolvedValue(allowedAuthorization(" Needs-AI "));
1451
+
1452
+ const result: GitHubWebhookHandlingResult = await handle({
1453
+ event: "issues",
1454
+ payload: issuesLabeledPayload({ labelName: "needs-ai" }),
1455
+ });
1456
+
1457
+ expect(result.outcome).toBe("queued");
1458
+ expect(enqueueArguments(1).taskType).toBe(CodeFixTaskType.GitHubIssueFix);
1459
+ });
1460
+
1461
+ test("a different label starts nothing and says nothing", async () => {
1462
+ authorizeSpy.mockResolvedValue(allowedAuthorization("needs-ai"));
1463
+
1464
+ const result: GitHubWebhookHandlingResult = await handle({
1465
+ event: "issues",
1466
+ payload: issuesLabeledPayload({ labelName: "bug" }),
1467
+ });
1468
+
1469
+ expect(result.handled).toBe(false);
1470
+ expect(result.outcome).toBe("label-is-not-the-trigger-label");
1471
+ expectNoRunAndNoWrites();
1472
+ });
1473
+
1474
+ /*
1475
+ * With a label configured, the default must NOT also fire — a repository
1476
+ * that renamed its trigger label would otherwise still answer the old one.
1477
+ */
1478
+ test("the default label stops working once another one is configured", async () => {
1479
+ authorizeSpy.mockResolvedValue(allowedAuthorization("needs-ai"));
1480
+
1481
+ const result: GitHubWebhookHandlingResult = await handle({
1482
+ event: "issues",
1483
+ payload: issuesLabeledPayload({ labelName: "oneuptime" }),
1484
+ });
1485
+
1486
+ expect(result.outcome).toBe("label-is-not-the-trigger-label");
1487
+ expect(enqueueRunSpy).not.toHaveBeenCalled();
1488
+ });
1489
+
1490
+ test("an all-whitespace configured label falls back to the default", async () => {
1491
+ authorizeSpy.mockResolvedValue(allowedAuthorization(" "));
1492
+
1493
+ const result: GitHubWebhookHandlingResult = await handle({
1494
+ event: "issues",
1495
+ payload: issuesLabeledPayload({ labelName: "oneuptime" }),
1496
+ });
1497
+
1498
+ expect(result.outcome).toBe("queued");
1499
+ });
1500
+
1501
+ test("an issues event with no label object starts nothing", async () => {
1502
+ const result: GitHubWebhookHandlingResult = await handle({
1503
+ event: "issues",
1504
+ payload: {
1505
+ ...webhookEnvelope(),
1506
+ action: "labeled",
1507
+ issue: { number: ISSUE_NUMBER, title: "Checkout throws" },
1508
+ },
1509
+ });
1510
+
1511
+ expect(result.outcome).toBe("label-is-not-the-trigger-label");
1512
+ expectNoRunAndNoWrites();
1513
+ });
1514
+
1515
+ /*
1516
+ * Most `issues` deliveries are opened / edited / closed, and authorizing
1517
+ * costs a database read plus a GitHub permission call. Doing that on every
1518
+ * issue anyone touches in every connected repository burns the
1519
+ * installation's rate limit on deliveries that were never going to do
1520
+ * anything — so the cheap action check has to come first.
1521
+ */
1522
+ test.each(["opened", "edited", "closed", "reopened", "unassigned"])(
1523
+ "an issues %s event is dropped before anything expensive happens",
1524
+ async (action: string) => {
1525
+ const result: GitHubWebhookHandlingResult = await handle({
1526
+ event: "issues",
1527
+ payload: {
1528
+ ...webhookEnvelope(),
1529
+ action: action,
1530
+ issue: { number: ISSUE_NUMBER, title: "Checkout throws" },
1531
+ },
1532
+ });
1533
+
1534
+ expect(result.outcome).toBe("action-not-actionable");
1535
+ expect(authorizeSpy).not.toHaveBeenCalled();
1536
+ expect(getAppSlugSpy).not.toHaveBeenCalled();
1537
+ expectNoRunAndNoWrites();
1538
+ },
1539
+ );
1540
+
1541
+ /*
1542
+ * GitHub models a pull request as an issue, so labelling a pull request
1543
+ * arrives here too — with the real event following on `pull_request`.
1544
+ * Acting on both would run the work twice.
1545
+ */
1546
+ test("an issues event that is really a pull request is left to the pull_request event", async () => {
1547
+ const result: GitHubWebhookHandlingResult = await handle({
1548
+ event: "issues",
1549
+ payload: issuesLabeledPayload({
1550
+ labelName: "oneuptime",
1551
+ isReallyAPullRequest: true,
1552
+ }),
1553
+ });
1554
+
1555
+ expect(result.handled).toBe(false);
1556
+ expect(result.outcome).toBe("issue-is-pull-request");
1557
+ expect(authorizeSpy).not.toHaveBeenCalled();
1558
+ expectNoRunAndNoWrites();
1559
+ });
1560
+
1561
+ test("an issues event with no issue number is dropped before authorizing", async () => {
1562
+ const result: GitHubWebhookHandlingResult = await handle({
1563
+ event: "issues",
1564
+ payload: {
1565
+ ...webhookEnvelope(),
1566
+ action: "labeled",
1567
+ issue: { title: "Checkout throws" },
1568
+ label: { name: "oneuptime" },
1569
+ },
1570
+ });
1571
+
1572
+ expect(result.outcome).toBe("no-issue-number");
1573
+ expect(authorizeSpy).not.toHaveBeenCalled();
1574
+ });
1575
+ });
1576
+
1577
+ describe("an issue assigned to the app", () => {
1578
+ test("an assignment to the app's bot login starts an Implement run", async () => {
1579
+ const result: GitHubWebhookHandlingResult = await handle({
1580
+ event: "issues",
1581
+ payload: issuesAssignedPayload({ assigneeLogin: BOT_LOGIN }),
1582
+ });
1583
+
1584
+ expect(result.handled).toBe(true);
1585
+ expect(result.outcome).toBe("queued");
1586
+ expect(enqueueArguments(1).taskType).toBe(CodeFixTaskType.GitHubIssueFix);
1587
+ expect(enqueuedContext(1).issueNumber).toBe(ISSUE_NUMBER);
1588
+ // Nobody wrote a sentence, so the agent is told to read the issue.
1589
+ expect(enqueuedContext(1).instruction).toBe("");
1590
+ expect(enqueuedContext(1).triggerCommentId).toBeUndefined();
1591
+ });
1592
+
1593
+ test("the bot login is matched case-insensitively", async () => {
1594
+ const result: GitHubWebhookHandlingResult = await handle({
1595
+ event: "issues",
1596
+ payload: issuesAssignedPayload({ assigneeLogin: "OneUptime[Bot]" }),
1597
+ });
1598
+
1599
+ expect(result.outcome).toBe("queued");
1600
+ });
1601
+
1602
+ /*
1603
+ * Assigning a teammate an issue in a connected repository must not start
1604
+ * an agent run. This is the single check standing between "normal triage"
1605
+ * and "every assignment spends AI budget".
1606
+ */
1607
+ test.each([
1608
+ "octo-dev",
1609
+ "oneuptime",
1610
+ "oneuptime-staging[bot]",
1611
+ "someone[bot]",
1612
+ ])("an assignment to %s starts nothing", async (assigneeLogin: string) => {
1613
+ const result: GitHubWebhookHandlingResult = await handle({
1614
+ event: "issues",
1615
+ payload: issuesAssignedPayload({ assigneeLogin: assigneeLogin }),
1616
+ });
1617
+
1618
+ expect(result.handled).toBe(false);
1619
+ expect(result.outcome).toBe("assignee-is-not-this-app");
1620
+ // Answered from the payload alone: no database read, no GitHub call.
1621
+ expect(authorizeSpy).not.toHaveBeenCalled();
1622
+ expectNoRunAndNoWrites();
1623
+ });
1624
+
1625
+ test("an assignment event with no assignee object starts nothing", async () => {
1626
+ const result: GitHubWebhookHandlingResult = await handle({
1627
+ event: "issues",
1628
+ payload: issuesAssignedPayload({}),
1629
+ });
1630
+
1631
+ expect(result.outcome).toBe("assignee-is-not-this-app");
1632
+ expectNoRunAndNoWrites();
1633
+ });
1634
+
1635
+ /*
1636
+ * Without a slug there is no bot login to compare against, and "unknown"
1637
+ * must never compare equal to "the app".
1638
+ */
1639
+ test("an unresolvable app slug starts nothing", async () => {
1640
+ getAppSlugSpy.mockResolvedValue(null);
1641
+
1642
+ const result: GitHubWebhookHandlingResult = await handle({
1643
+ event: "issues",
1644
+ payload: issuesAssignedPayload({ assigneeLogin: BOT_LOGIN }),
1645
+ });
1646
+
1647
+ expect(result.outcome).toBe("assignee-is-not-this-app");
1648
+ expectNoRunAndNoWrites();
1649
+ });
1650
+ });
1651
+
1652
+ describe("a review requested from the app", () => {
1653
+ test("a review request for the app's bot login queues a review run", async () => {
1654
+ const result: GitHubWebhookHandlingResult = await handle({
1655
+ event: "pull_request",
1656
+ payload: reviewRequestedPayload({
1657
+ requestedReviewerLogin: BOT_LOGIN,
1658
+ }),
1659
+ });
1660
+
1661
+ expect(result.handled).toBe(true);
1662
+ expect(result.outcome).toBe("queued");
1663
+ expect(enqueueArguments(1).taskType).toBe(
1664
+ CodeFixTaskType.GitHubPullRequestReview,
1665
+ );
1666
+ expect(enqueuedContext(1).pullRequestNumber).toBe(PULL_REQUEST_NUMBER);
1667
+ expect(enqueuedContext(1).pullRequestHeadRefName).toBe(HEAD_REF_NAME);
1668
+ });
1669
+
1670
+ /*
1671
+ * Requesting a human reviewer is the single most common pull request
1672
+ * event in an active repository. Answering it would start an agent run
1673
+ * every time anyone asked a colleague to look at their code.
1674
+ */
1675
+ test("a review request for a human starts nothing and never even authorizes", async () => {
1676
+ const result: GitHubWebhookHandlingResult = await handle({
1677
+ event: "pull_request",
1678
+ payload: reviewRequestedPayload({
1679
+ requestedReviewerLogin: "some-reviewer",
1680
+ }),
1681
+ });
1682
+
1683
+ expect(result.handled).toBe(false);
1684
+ expect(result.outcome).toBe("reviewer-is-not-this-app");
1685
+ expect(authorizeSpy).not.toHaveBeenCalled();
1686
+ expectNoRunAndNoWrites();
1687
+ });
1688
+
1689
+ test("a review requested from a TEAM (no requested_reviewer) starts nothing", async () => {
1690
+ const result: GitHubWebhookHandlingResult = await handle({
1691
+ event: "pull_request",
1692
+ payload: reviewRequestedPayload({}),
1693
+ });
1694
+
1695
+ expect(result.outcome).toBe("reviewer-is-not-this-app");
1696
+ expectNoRunAndNoWrites();
1697
+ });
1698
+
1699
+ test.each(["opened", "synchronize", "closed", "labeled"])(
1700
+ "a pull_request %s event starts nothing",
1701
+ async (action: string) => {
1702
+ const result: GitHubWebhookHandlingResult = await handle({
1703
+ event: "pull_request",
1704
+ payload: reviewRequestedPayload({
1705
+ requestedReviewerLogin: BOT_LOGIN,
1706
+ action: action,
1707
+ }),
1708
+ });
1709
+
1710
+ expect(result.handled).toBe(false);
1711
+ expect(result.outcome).toBe("action-not-actionable");
1712
+ expectNoRunAndNoWrites();
1713
+ },
1714
+ );
1715
+ });
1716
+
1717
+ /*
1718
+ * "Already working on this" and "over the daily budget" are normal answers,
1719
+ * not failures — and useless unless the person who asked can read them. A
1720
+ * command that produces no visible response reads as a broken integration.
1721
+ */
1722
+ describe("when the enqueue refuses", () => {
1723
+ test("a BadDataException becomes a refusal comment carrying its message", async () => {
1724
+ enqueueRunSpy.mockRejectedValue(
1725
+ new BadDataException(
1726
+ "I am already working on this — I will comment here when I am done.",
1727
+ ),
1728
+ );
1729
+
1730
+ const result: GitHubWebhookHandlingResult = await handle({
1731
+ event: "issue_comment",
1732
+ payload: issueCommentPayload({ body: `@${APP_SLUG} implement this` }),
1733
+ });
1734
+
1735
+ expect(result.handled).toBe(true);
1736
+ expect(result.outcome).toBe("refused");
1737
+ expect(createIssueCommentSpy).toHaveBeenCalledTimes(1);
1738
+ expect(commentArguments(1).body).toContain("already working on this");
1739
+ expect(commentArguments(1).issueNumber).toBe(ISSUE_NUMBER);
1740
+ expect(acknowledgeSpy).not.toHaveBeenCalled();
1741
+ expect(recordAcknowledgementSpy).not.toHaveBeenCalled();
1742
+ });
1743
+
1744
+ test("an over-budget refusal is reported in the thread too", async () => {
1745
+ enqueueRunSpy.mockRejectedValue(
1746
+ new BadDataException(
1747
+ "This project is over its daily AI fix-run budget.",
1748
+ ),
1749
+ );
1750
+
1751
+ const result: GitHubWebhookHandlingResult = await handle({
1752
+ event: "issue_comment",
1753
+ payload: pullRequestCommentPayload({ body: `@${APP_SLUG} review` }),
1754
+ });
1755
+
1756
+ expect(result.outcome).toBe("refused");
1757
+ expect(commentArguments(1).body).toContain("daily AI fix-run budget");
1758
+ });
1759
+
1760
+ // A thrown non-Error must still produce words a human can act on.
1761
+ test("a non-Error rejection still gets a plain-English refusal", async () => {
1762
+ enqueueRunSpy.mockRejectedValue("something odd");
1763
+
1764
+ const result: GitHubWebhookHandlingResult = await handle({
1765
+ event: "issue_comment",
1766
+ payload: issueCommentPayload({ body: `@${APP_SLUG} implement this` }),
1767
+ });
1768
+
1769
+ expect(result.outcome).toBe("refused");
1770
+ expect(commentArguments(1).body).toContain(
1771
+ "I could not start that just now",
1772
+ );
1773
+ });
1774
+
1775
+ /*
1776
+ * The reply is best effort. GitHub being unavailable for the comment must
1777
+ * not turn a clean refusal into a non-2xx and a redelivery.
1778
+ */
1779
+ test("a refusal whose comment fails to post does not fail the delivery", async () => {
1780
+ enqueueRunSpy.mockRejectedValue(new BadDataException("Already running."));
1781
+ createIssueCommentSpy.mockRejectedValue(new Error("503 from GitHub"));
1782
+
1783
+ const result: GitHubWebhookHandlingResult = await handle({
1784
+ event: "issue_comment",
1785
+ payload: issueCommentPayload({ body: `@${APP_SLUG} implement this` }),
1786
+ });
1787
+
1788
+ expect(result.handled).toBe(true);
1789
+ expect(result.outcome).toBe("refused");
1790
+ });
1791
+ });
1792
+
1793
+ describe("authorization outcomes", () => {
1794
+ test("passes the sender through so the authorizer can apply its own rules", async () => {
1795
+ await handle({
1796
+ event: "issue_comment",
1797
+ payload: issueCommentPayload({ body: `@${APP_SLUG} implement this` }),
1798
+ });
1799
+
1800
+ const authorization: {
1801
+ organizationName: string;
1802
+ repositoryName: string;
1803
+ installationId: string;
1804
+ senderLogin: string;
1805
+ senderType: string | undefined;
1806
+ } = nthCallArgument(authorizeSpy, 1) as {
1807
+ organizationName: string;
1808
+ repositoryName: string;
1809
+ installationId: string;
1810
+ senderLogin: string;
1811
+ senderType: string | undefined;
1812
+ };
1813
+
1814
+ expect(authorization.senderLogin).toBe(SENDER_LOGIN);
1815
+ expect(authorization.senderType).toBe("User");
1816
+ expect(authorization.installationId).toBe(INSTALLATION_ID);
1817
+ expect(authorization.organizationName).toBe(ORGANIZATION_NAME);
1818
+ expect(authorization.repositoryName).toBe(REPOSITORY_NAME);
1819
+ });
1820
+
1821
+ /*
1822
+ * THE anti-amplification rule. Replying to a command from an account
1823
+ * without write access would make this app a comment-poster that any
1824
+ * GitHub account can drive at a repository it cannot write to: one
1825
+ * mention, one guaranteed comment, repeat.
1826
+ */
1827
+ test("insufficient permission gets a reaction on the comment and NO comment", async () => {
1828
+ authorizeSpy.mockResolvedValue({
1829
+ outcome: GitHubAuthorizationOutcome.InsufficientPermission,
1830
+ codeRepository: connectedRepository(),
1831
+ });
1832
+
1833
+ const result: GitHubWebhookHandlingResult = await handle({
1834
+ event: "issue_comment",
1835
+ payload: issueCommentPayload({
1836
+ body: `@${APP_SLUG} implement this`,
1837
+ commentId: 4242,
1838
+ }),
1839
+ });
1840
+
1841
+ expect(result.handled).toBe(false);
1842
+ expect(result.outcome).toBe("unresolvable-context");
1843
+ expect(createIssueCommentSpy).not.toHaveBeenCalled();
1844
+ expect(enqueueRunSpy).not.toHaveBeenCalled();
1845
+ expect(addReactionToCommentSpy).toHaveBeenCalledTimes(1);
1846
+ expect(reactionArguments(1).reaction).toBe(GitHubReaction.Confused);
1847
+ expect(reactionArguments(1).commentId).toBe(4242);
1848
+ expect(reactionArguments(1).installationId).toBe(INSTALLATION_ID);
1849
+ });
1850
+
1851
+ test("insufficient permission on an event with no comment says nothing at all", async () => {
1852
+ authorizeSpy.mockResolvedValue({
1853
+ outcome: GitHubAuthorizationOutcome.InsufficientPermission,
1854
+ codeRepository: connectedRepository(),
1855
+ });
1856
+
1857
+ const result: GitHubWebhookHandlingResult = await handle({
1858
+ event: "issues",
1859
+ payload: issuesLabeledPayload({ labelName: "oneuptime" }),
1860
+ });
1861
+
1862
+ expect(result.handled).toBe(false);
1863
+ expect(result.outcome).toBe("unresolvable-context");
1864
+ expectNoRunAndNoWrites();
1865
+ });
1866
+
1867
+ /*
1868
+ * The opposite rule, and the reason the two outcomes are separate: a
1869
+ * collaborator whose repository has the switch off is told where the
1870
+ * switch is, or the integration reads as broken.
1871
+ */
1872
+ test("commands disabled DOES reply, and starts nothing", async () => {
1873
+ authorizeSpy.mockResolvedValue({
1874
+ outcome: GitHubAuthorizationOutcome.CommandsDisabled,
1875
+ codeRepository: connectedRepository(),
1876
+ reason:
1877
+ "GitHub commands are switched off for this repository. A project admin can turn them back on.",
1878
+ });
1879
+
1880
+ const result: GitHubWebhookHandlingResult = await handle({
1881
+ event: "issue_comment",
1882
+ payload: issueCommentPayload({ body: `@${APP_SLUG} implement this` }),
1883
+ });
1884
+
1885
+ /*
1886
+ * handled is TRUE: the app did something visible in the thread, which is
1887
+ * the whole point of separating this outcome from Ignore.
1888
+ */
1889
+ expect(result.handled).toBe(true);
1890
+ expect(result.outcome).toBe("commands-disabled");
1891
+ expect(enqueueRunSpy).not.toHaveBeenCalled();
1892
+ expect(addReactionToCommentSpy).not.toHaveBeenCalled();
1893
+ expect(createIssueCommentSpy).toHaveBeenCalledTimes(1);
1894
+ expect(commentArguments(1).body).toContain("switched off");
1895
+ expect(commentArguments(1).issueNumber).toBe(ISSUE_NUMBER);
1896
+ });
1897
+
1898
+ /*
1899
+ * ...but ONLY when the app was actually addressed. This runs for every
1900
+ * label added to every issue in the repository, and answering here turned
1901
+ * a repository with the switch off into one where labelling anything at
1902
+ * all produced a bot comment.
1903
+ */
1904
+ test("commands disabled says NOTHING when a non-trigger label is added", async () => {
1905
+ authorizeSpy.mockResolvedValue({
1906
+ outcome: GitHubAuthorizationOutcome.CommandsDisabled,
1907
+ codeRepository: connectedRepository(),
1908
+ reason: "GitHub commands are switched off for this repository.",
1909
+ });
1910
+
1911
+ const result: GitHubWebhookHandlingResult = await handle({
1912
+ event: "issues",
1913
+ payload: issuesLabeledPayload({ labelName: "needs-triage" }),
1914
+ });
1915
+
1916
+ expect(result.handled).toBe(false);
1917
+ expect(result.outcome).toBe("label-is-not-the-trigger-label");
1918
+ expect(createIssueCommentSpy).not.toHaveBeenCalled();
1919
+ });
1920
+
1921
+ test("commands disabled DOES reply when the trigger label is added", async () => {
1922
+ authorizeSpy.mockResolvedValue({
1923
+ outcome: GitHubAuthorizationOutcome.CommandsDisabled,
1924
+ codeRepository: connectedRepository(),
1925
+ reason: "GitHub commands are switched off for this repository.",
1926
+ });
1927
+
1928
+ const result: GitHubWebhookHandlingResult = await handle({
1929
+ event: "issues",
1930
+ payload: issuesLabeledPayload({ labelName: "oneuptime" }),
1931
+ });
1932
+
1933
+ expect(result.handled).toBe(true);
1934
+ expect(result.outcome).toBe("commands-disabled");
1935
+ expect(enqueueRunSpy).not.toHaveBeenCalled();
1936
+ expect(createIssueCommentSpy).toHaveBeenCalledTimes(1);
1937
+ });
1938
+
1939
+ test("commands disabled with no reason still says something useful", async () => {
1940
+ authorizeSpy.mockResolvedValue({
1941
+ outcome: GitHubAuthorizationOutcome.CommandsDisabled,
1942
+ codeRepository: connectedRepository(),
1943
+ });
1944
+
1945
+ await handle({
1946
+ event: "issue_comment",
1947
+ payload: issueCommentPayload({ body: `@${APP_SLUG} implement this` }),
1948
+ });
1949
+
1950
+ expect(commentArguments(1).body).toContain(
1951
+ "GitHub commands are switched off for this repository.",
1952
+ );
1953
+ });
1954
+
1955
+ /*
1956
+ * Ignore is the loop guard and the "not our repository" answer. It must be
1957
+ * completely silent: this app has no standing to comment in a repository
1958
+ * no OneUptime project has connected, and answering another bot is how a
1959
+ * budget disappears overnight.
1960
+ */
1961
+ test("ignore is completely silent — no comment, no reaction, no run", async () => {
1962
+ authorizeSpy.mockResolvedValue({
1963
+ outcome: GitHubAuthorizationOutcome.Ignore,
1964
+ });
1965
+
1966
+ const result: GitHubWebhookHandlingResult = await handle({
1967
+ event: "issue_comment",
1968
+ payload: issueCommentPayload({ body: `@${APP_SLUG} implement this` }),
1969
+ });
1970
+
1971
+ expect(result.handled).toBe(false);
1972
+ expect(result.outcome).toBe("unresolvable-context");
1973
+ expectNoRunAndNoWrites();
1974
+ });
1975
+
1976
+ /*
1977
+ * An Allowed outcome whose repository row carries no id cannot be billed
1978
+ * to a project or deduped against one. Starting a run anyway would create
1979
+ * an AIRun no worker could execute.
1980
+ */
1981
+ test("an allowed outcome with no repository row starts nothing", async () => {
1982
+ authorizeSpy.mockResolvedValue({
1983
+ outcome: GitHubAuthorizationOutcome.Allowed,
1984
+ projectName: PROJECT_NAME,
1985
+ });
1986
+
1987
+ const result: GitHubWebhookHandlingResult = await handle({
1988
+ event: "issue_comment",
1989
+ payload: issueCommentPayload({ body: `@${APP_SLUG} implement this` }),
1990
+ });
1991
+
1992
+ expect(result.handled).toBe(false);
1993
+ expect(result.outcome).toBe("no-repository");
1994
+ expect(enqueueRunSpy).not.toHaveBeenCalled();
1995
+ });
1996
+ });
1997
+
1998
+ /*
1999
+ * Every field read here comes out of a JSON blob an untrusted party can
2000
+ * influence. None of these shapes may throw, and none may be acted on.
2001
+ */
2002
+ describe("malformed and adversarial payloads", () => {
2003
+ // A mention whose envelope is missing one of the three required keys.
2004
+ function commentPayloadMissing(data: {
2005
+ repository?: JSONObject;
2006
+ installation?: JSONObject;
2007
+ sender?: JSONObject;
2008
+ }): JSONObject {
2009
+ const payload: JSONObject = {
2010
+ action: "created",
2011
+ issue: { number: ISSUE_NUMBER, title: "Checkout throws" },
2012
+ comment: {
2013
+ id: TRIGGER_COMMENT_ID,
2014
+ body: `@${APP_SLUG} implement this`,
2015
+ },
2016
+ };
2017
+
2018
+ if (data.repository) {
2019
+ payload["repository"] = data.repository;
2020
+ }
2021
+
2022
+ if (data.installation) {
2023
+ payload["installation"] = data.installation;
2024
+ }
2025
+
2026
+ if (data.sender) {
2027
+ payload["sender"] = data.sender;
2028
+ }
2029
+
2030
+ return payload;
2031
+ }
2032
+
2033
+ test("a payload with no repository never reaches the authorizer", async () => {
2034
+ const result: GitHubWebhookHandlingResult = await handle({
2035
+ event: "issue_comment",
2036
+ payload: commentPayloadMissing({
2037
+ installation: { id: INSTALLATION_ID_NUMBER },
2038
+ sender: { login: SENDER_LOGIN, type: "User" },
2039
+ }),
2040
+ });
2041
+
2042
+ expect(result.outcome).toBe("unresolvable-context");
2043
+ expect(authorizeSpy).not.toHaveBeenCalled();
2044
+ expectNoRunAndNoWrites();
2045
+ });
2046
+
2047
+ test.each(["checkout", "/checkout", "acme/", "", " "])(
2048
+ "a repository full_name of %p is not a repository",
2049
+ async (fullName: string) => {
2050
+ const result: GitHubWebhookHandlingResult = await handle({
2051
+ event: "issue_comment",
2052
+ payload: {
2053
+ ...issueCommentPayload({ body: `@${APP_SLUG} implement this` }),
2054
+ repository: { full_name: fullName },
2055
+ },
2056
+ });
2057
+
2058
+ expect(result.outcome).toBe("unresolvable-context");
2059
+ expect(authorizeSpy).not.toHaveBeenCalled();
2060
+ },
2061
+ );
2062
+
2063
+ /*
2064
+ * No installation means no credential to act with. This is the value the
2065
+ * whole authorization chain is anchored on, so an absent one can never be
2066
+ * defaulted or inferred.
2067
+ */
2068
+ test("a payload with no installation never reaches the authorizer", async () => {
2069
+ const result: GitHubWebhookHandlingResult = await handle({
2070
+ event: "issue_comment",
2071
+ payload: commentPayloadMissing({
2072
+ repository: { full_name: REPOSITORY_FULL_NAME },
2073
+ sender: { login: SENDER_LOGIN, type: "User" },
2074
+ }),
2075
+ });
2076
+
2077
+ expect(result.outcome).toBe("unresolvable-context");
2078
+ expect(authorizeSpy).not.toHaveBeenCalled();
2079
+ });
2080
+
2081
+ test("a payload with no sender never reaches the authorizer", async () => {
2082
+ const result: GitHubWebhookHandlingResult = await handle({
2083
+ event: "issue_comment",
2084
+ payload: commentPayloadMissing({
2085
+ repository: { full_name: REPOSITORY_FULL_NAME },
2086
+ installation: { id: INSTALLATION_ID_NUMBER },
2087
+ }),
2088
+ });
2089
+
2090
+ expect(result.outcome).toBe("unresolvable-context");
2091
+ expect(authorizeSpy).not.toHaveBeenCalled();
2092
+ });
2093
+
2094
+ test("a sender with no login never reaches the authorizer", async () => {
2095
+ const result: GitHubWebhookHandlingResult = await handle({
2096
+ event: "issue_comment",
2097
+ payload: commentPayloadMissing({
2098
+ repository: { full_name: REPOSITORY_FULL_NAME },
2099
+ installation: { id: INSTALLATION_ID_NUMBER },
2100
+ sender: { type: "User" },
2101
+ }),
2102
+ });
2103
+
2104
+ expect(result.outcome).toBe("unresolvable-context");
2105
+ expect(authorizeSpy).not.toHaveBeenCalled();
2106
+ });
2107
+
2108
+ test("a comment with an empty body is dropped before anything else", async () => {
2109
+ const result: GitHubWebhookHandlingResult = await handle({
2110
+ event: "issue_comment",
2111
+ payload: issueCommentPayload({ body: "" }),
2112
+ });
2113
+
2114
+ expect(result.outcome).toBe("no-comment-body");
2115
+ expect(authorizeSpy).not.toHaveBeenCalled();
2116
+ expectNoRunAndNoWrites();
2117
+ });
2118
+
2119
+ test("a comment event with no comment object at all is dropped", async () => {
2120
+ const result: GitHubWebhookHandlingResult = await handle({
2121
+ event: "issue_comment",
2122
+ payload: {
2123
+ ...webhookEnvelope(),
2124
+ action: "created",
2125
+ issue: { number: ISSUE_NUMBER, title: "Checkout throws" },
2126
+ },
2127
+ });
2128
+
2129
+ expect(result.outcome).toBe("no-comment-body");
2130
+ expectNoRunAndNoWrites();
2131
+ });
2132
+
2133
+ /*
2134
+ * A missing number means there is no thread to answer in. Defaulting it to
2135
+ * anything would post into a stranger's issue.
2136
+ */
2137
+ test("a comment on a conversation with no number is dropped", async () => {
2138
+ const result: GitHubWebhookHandlingResult = await handle({
2139
+ event: "issue_comment",
2140
+ payload: {
2141
+ ...webhookEnvelope(),
2142
+ action: "created",
2143
+ issue: { title: "Checkout throws" },
2144
+ comment: { id: 1, body: `@${APP_SLUG} implement this` },
2145
+ },
2146
+ });
2147
+
2148
+ expect(result.outcome).toBe("no-conversation-number");
2149
+ expectNoRunAndNoWrites();
2150
+ });
2151
+
2152
+ /*
2153
+ * GitHub sends numbers as JSON numbers. A string here means the payload is
2154
+ * not the shape it claims to be, and guessing at it is how a run ends up
2155
+ * pointed at the wrong conversation.
2156
+ */
2157
+ test("a conversation number sent as a string is not accepted", async () => {
2158
+ const result: GitHubWebhookHandlingResult = await handle({
2159
+ event: "issue_comment",
2160
+ payload: {
2161
+ ...webhookEnvelope(),
2162
+ action: "created",
2163
+ issue: { number: "12", title: "Checkout throws" },
2164
+ comment: { id: 1, body: `@${APP_SLUG} implement this` },
2165
+ },
2166
+ });
2167
+
2168
+ expect(result.outcome).toBe("no-conversation-number");
2169
+ expectNoRunAndNoWrites();
2170
+ });
2171
+
2172
+ test("an instance that cannot resolve its own app slug answers no comment", async () => {
2173
+ getAppSlugSpy.mockResolvedValue(null);
2174
+
2175
+ const result: GitHubWebhookHandlingResult = await handle({
2176
+ event: "issue_comment",
2177
+ payload: issueCommentPayload({ body: `@${APP_SLUG} implement this` }),
2178
+ });
2179
+
2180
+ expect(result.handled).toBe(false);
2181
+ expect(result.outcome).toBe("no-app-slug");
2182
+ expect(authorizeSpy).not.toHaveBeenCalled();
2183
+ expectNoRunAndNoWrites();
2184
+ });
2185
+
2186
+ /*
2187
+ * A comment that is nothing but a fenced block containing a mention — for
2188
+ * example someone pasting the help text back into the thread.
2189
+ */
2190
+ test("a mention inside a fenced code block is not a command", async () => {
2191
+ const result: GitHubWebhookHandlingResult = await handle({
2192
+ event: "issue_comment",
2193
+ payload: issueCommentPayload({
2194
+ body: `Try this:\n\n\`\`\`text\n@${APP_SLUG} implement this\n\`\`\`\n`,
2195
+ }),
2196
+ });
2197
+
2198
+ expect(result.outcome).toBe("no-mention");
2199
+ expectNoRunAndNoWrites();
2200
+ });
2201
+ });
2202
+ });