@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.
- package/Models/DatabaseModels/Alert.ts +55 -0
- package/Models/DatabaseModels/CloudResource.ts +45 -7
- package/Models/DatabaseModels/CodeRepository.ts +85 -0
- package/Models/DatabaseModels/GoogleSecOpsConnection.ts +57 -7
- package/Models/DatabaseModels/GoogleSecOpsConnectionRun.ts +263 -0
- package/Models/DatabaseModels/Incident.ts +55 -0
- package/Models/DatabaseModels/Index.ts +16 -0
- package/Models/DatabaseModels/ScheduledMaintenance.ts +55 -0
- package/Models/DatabaseModels/User.ts +51 -2
- package/Models/DatabaseModels/UserNotificationEmailRollupItem.ts +43 -2
- package/Models/DatabaseModels/UserWebAuthn.ts +17 -0
- package/Models/DatabaseModels/VMwareResource.ts +1149 -0
- package/Models/DatabaseModels/VMwareVCenter.ts +1122 -0
- package/Models/DatabaseModels/VMwareVCenterFeed.ts +625 -0
- package/Models/DatabaseModels/VMwareVCenterLabelRule.ts +514 -0
- package/Models/DatabaseModels/VMwareVCenterOwnerRule.ts +596 -0
- package/Models/DatabaseModels/VMwareVCenterOwnerTeam.ts +485 -0
- package/Models/DatabaseModels/VMwareVCenterOwnerUser.ts +484 -0
- package/Models/DatabaseModels/WorkflowVariable.ts +22 -1
- package/Server/API/AIAgentDataAPI.ts +79 -4
- package/Server/API/AIAgentGitHubAPI.ts +523 -0
- package/Server/API/AIChatAPI.ts +20 -1
- package/Server/API/BillingAPI.ts +33 -0
- package/Server/API/DashboardAPI.ts +10 -0
- package/Server/API/GitHubAPI.ts +47 -5
- package/Server/API/GoogleSecOpsConnectionAPI.ts +79 -0
- package/Server/API/UserWebAuthnAPI.ts +21 -1
- package/Server/API/VMwareResourceAPI.ts +143 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1791800000000-SplitWebAuthnChallengeByPurpose.ts +68 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1791900000000-AddSeverityAndStateToNotificationEmailRollup.ts +26 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1792000000000-AddVMwareTables.ts +636 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1792100000000-AddGitHubCommandSettingsToCodeRepository.ts +47 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1792200000000-AddUserWebAuthnPurpose.ts +17 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1792300000000-AddGoogleSecOpsDiagnostics.ts +83 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +12 -0
- package/Server/Middleware/IdentityRateLimit.ts +101 -0
- package/Server/Services/AIRunService.ts +13 -0
- package/Server/Services/BillingPaymentMethodService.ts +99 -28
- package/Server/Services/BillingService.ts +287 -20
- package/Server/Services/CloudResourceInstanceService.ts +70 -0
- package/Server/Services/ExceptionAggregationService.ts +1 -0
- package/Server/Services/GoogleSecOpsConnectionRunService.ts +10 -0
- package/Server/Services/GoogleSecOpsConnectionService.ts +12 -0
- package/Server/Services/Index.ts +16 -0
- package/Server/Services/LogAggregationService.ts +1 -0
- package/Server/Services/MetricAggregationService.ts +1 -0
- package/Server/Services/MobilePasskeyLoginService.ts +216 -0
- package/Server/Services/MonitorService.ts +51 -1
- package/Server/Services/MonitorTemplateService.ts +91 -29
- package/Server/Services/NetworkDeviceAutoImportRuleEngineService.ts +132 -1
- package/Server/Services/NetworkDeviceAutoImportRuleService.ts +176 -2
- package/Server/Services/OpenTelemetryIngestService.ts +17 -0
- package/Server/Services/PayAsYouGoBillingService.ts +446 -0
- package/Server/Services/ProjectService.ts +46 -2
- package/Server/Services/ServerlessFunctionInstanceService.ts +71 -0
- package/Server/Services/TeamMemberService.ts +96 -36
- package/Server/Services/TelemetryIngestionKeyService.ts +80 -10
- package/Server/Services/TelemetryUsageBillingService.ts +95 -5
- package/Server/Services/TraceAggregationService.ts +5 -4
- package/Server/Services/UserWebAuthnService.ts +592 -133
- package/Server/Services/VMwareResourceService.ts +648 -0
- package/Server/Services/VMwareVCenterFeedService.ts +99 -0
- package/Server/Services/VMwareVCenterLabelRuleEngineService.ts +240 -0
- package/Server/Services/VMwareVCenterLabelRuleService.ts +14 -0
- package/Server/Services/VMwareVCenterOwnerRuleEngineService.ts +252 -0
- package/Server/Services/VMwareVCenterOwnerRuleService.ts +14 -0
- package/Server/Services/VMwareVCenterOwnerTeamService.ts +139 -0
- package/Server/Services/VMwareVCenterOwnerUserService.ts +133 -0
- package/Server/Services/VMwareVCenterService.ts +629 -0
- package/Server/Services/WorkflowVariableService.ts +203 -0
- package/Server/Types/Billing/MeteredPlan/ActiveMonitoringMeteredPlan.ts +17 -0
- package/Server/Types/Billing/MeteredPlan/TelemetryMeteredPlan.ts +31 -1
- package/Server/Utils/CodeRepository/GitHub/GitHub.ts +7 -0
- package/Server/Utils/CodeRepository/GitHub/GitHubAgentTaskTrigger.ts +341 -0
- package/Server/Utils/CodeRepository/GitHub/GitHubCommandAuthorizer.ts +274 -0
- package/Server/Utils/CodeRepository/GitHub/GitHubCommandParser.ts +375 -0
- package/Server/Utils/CodeRepository/GitHub/GitHubConversation.ts +846 -0
- package/Server/Utils/CodeRepository/GitHub/GitHubReplyComposer.ts +360 -0
- package/Server/Utils/CodeRepository/GitHub/GitHubRunReply.ts +421 -0
- package/Server/Utils/CodeRepository/GitHub/GitHubWebhookEvents.ts +140 -0
- package/Server/Utils/CodeRepository/GitHub/GitHubWebhookHandler.ts +906 -0
- package/Server/Utils/Dashboard/PublicDashboardResourceListPolicy.ts +143 -0
- package/Server/Utils/DataSource/EgressGuard.ts +160 -15
- package/Server/Utils/Database/TruncateColumnValue.ts +1 -1
- package/Server/Utils/EmailRollup/EmailRollupFlushRunner.ts +2 -0
- package/Server/Utils/EmailRollup/EmailRollupRenderer.ts +33 -0
- package/Server/Utils/EmailRollup/EmailRollupWriter.ts +44 -0
- package/Server/Utils/LogRedaction.ts +2 -1
- package/Server/Utils/Monitor/MonitorAlert.ts +1 -1
- package/Server/Utils/Monitor/MonitorCriteriaEvaluator.ts +302 -3
- package/Server/Utils/Monitor/MonitorMaintenanceSuppression.ts +26 -4
- package/Server/Utils/Monitor/MonitorResourceContext.ts +5 -3
- package/Server/Utils/Monitor/MonitorStepResourceIdentity.ts +14 -0
- package/Server/Utils/Monitor/MonitorTemplateUtil.ts +1 -0
- package/Server/Utils/Monitor/SeriesResourceLabels.ts +26 -0
- package/Server/Utils/Monitor/SeriesResourceLinker.ts +28 -5
- package/Server/Utils/ResourceFeed/ResourceFeedUtil.ts +3 -3
- package/Server/Utils/SecurityEvent/ConnectorErrorMessage.ts +14 -9
- package/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsClient.ts +116 -84
- package/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsPoller.ts +594 -195
- package/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsRunExecutor.ts +466 -0
- package/Server/Utils/Telemetry/EntityRegistry.ts +5 -0
- package/Server/Utils/Telemetry/ResourceHeartbeat.ts +2 -1
- package/Server/Utils/Telemetry/TelemetryEntity.ts +181 -1
- package/Server/Utils/Telemetry/VMwareSnapshotScan.ts +1352 -0
- package/Tests/App/Accounts/LoginPasskey.test.tsx +529 -0
- package/Tests/App/Accounts/MobilePasskey.test.tsx +620 -0
- package/Tests/App/Accounts/PasswordRequirements.test.tsx +254 -0
- package/Tests/App/Accounts/RegisterPassword.test.tsx +553 -0
- package/Tests/App/Dashboard/AskAiAvailability.test.ts +224 -0
- package/Tests/App/Dashboard/AskAiDisabled.test.tsx +432 -0
- package/Tests/App/Dashboard/BackupCodes.test.tsx +414 -0
- package/Tests/App/Dashboard/BillingPaidUsageFlow.test.tsx +530 -0
- package/Tests/App/Dashboard/BillingPaymentMethodForm.test.tsx +195 -0
- package/Tests/App/Dashboard/BreadcrumbCoverage.test.tsx +199 -0
- package/Tests/App/Dashboard/CloudDocumentationCard.test.tsx +198 -0
- package/Tests/App/Dashboard/CloudDocumentationMarkdown.test.ts +215 -0
- package/Tests/App/Dashboard/CloudFleetSummary.test.ts +183 -0
- package/Tests/App/Dashboard/CloudFleetSummaryComponent.test.tsx +248 -0
- package/Tests/App/Dashboard/CloudResourceTelemetryScope.test.ts +215 -0
- package/Tests/App/Dashboard/CriteriaFilterDefaults.test.ts +3 -0
- package/Tests/App/Dashboard/DashboardHeaderRequests.test.tsx +441 -0
- package/Tests/App/Dashboard/DashboardLazyPages.test.tsx +458 -0
- package/Tests/App/Dashboard/DashboardToolbarPopupLayering.test.tsx +566 -0
- package/Tests/App/Dashboard/DiscoveryScanLivePage.test.tsx +376 -0
- package/Tests/App/Dashboard/DiscoveryScanLiveUpdates.test.tsx +642 -0
- package/Tests/App/Dashboard/DiscoveryScanProgress.test.tsx +267 -0
- package/Tests/App/Dashboard/DiscoveryScanWizardValidation.test.tsx +69 -231
- package/Tests/App/Dashboard/GoogleSecOpsConnectionsErrors.test.tsx +244 -0
- package/Tests/App/Dashboard/GoogleSecOpsDiagnostics.test.tsx +724 -0
- package/Tests/App/Dashboard/MonitorTemplateSyncFields.test.tsx +539 -0
- package/Tests/App/Dashboard/OnCallLayerCardOverrides.test.tsx +24 -7
- package/Tests/App/Dashboard/OnCallOverridePresentation.test.ts +496 -0
- package/Tests/App/Dashboard/OnCallScheduleOverrideClarity.test.tsx +677 -0
- package/Tests/App/Dashboard/OnCallSchedulePreviewOverrides.test.tsx +31 -3
- package/Tests/App/Dashboard/PaidUsageConsent.test.tsx +180 -0
- package/Tests/App/Dashboard/PasskeySettings.test.tsx +1220 -0
- package/Tests/App/Dashboard/PayAsYouGoConsentGate.test.tsx +176 -275
- package/Tests/App/Dashboard/PayAsYouGoNotices.test.tsx +380 -233
- package/Tests/App/Dashboard/ProductMenuBreadcrumbs.test.tsx +3 -23
- package/Tests/App/Dashboard/ResourceFeed.test.tsx +1 -0
- package/Tests/App/Dashboard/ResourceFeedPageWiring.test.ts +8 -1
- package/Tests/App/Dashboard/TwoFactorStatus.test.tsx +260 -0
- package/Tests/App/Dashboard/VMwareDashboardWidgets.test.ts +573 -0
- package/Tests/App/Dashboard/WidgetCatalog.test.ts +2 -0
- package/Tests/App/Dashboard/WorkflowVariablesTable.test.tsx +768 -0
- package/Tests/App/StatusPage/StatusPageOverviewLiveAndSearch.test.tsx +532 -40
- package/Tests/App/StatusPage/StatusPageResourceSearchBox.test.tsx +19 -2
- package/Tests/Fixtures/MobilePasskeyUser.json +8 -0
- package/Tests/Models/DatabaseModels/ResourceFeedModels.test.ts +25 -6
- package/Tests/Models/DatabaseModels/VMwareModels.test.ts +1134 -0
- package/Tests/Models/RunnerVersionLabel.test.ts +4 -2
- package/Tests/Server/API/AIChatProvidersAIEnabled.test.ts +259 -0
- package/Tests/Server/API/AIChatRouteAuthorization.test.ts +31 -2
- package/Tests/Server/API/BillingPayAsYouGoStatus.test.ts +116 -0
- package/Tests/Server/API/DashboardPublicResourceListAPI.test.ts +29 -1
- package/Tests/Server/API/EnterpriseLicenseReportUserCount.test.ts +13 -2
- package/Tests/Server/API/GitHubWebhookSignature.test.ts +368 -0
- package/Tests/Server/API/GoogleSecOpsConnectionAPI.test.ts +248 -0
- package/Tests/Server/API/UserWebAuthnAuthenticationOptionsRoute.test.ts +330 -0
- package/Tests/Server/API/UserWebAuthnPasskeyAPI.test.ts +172 -0
- package/Tests/Server/API/VMwareResourceAPI.test.ts +546 -0
- package/Tests/Server/Infrastructure/Postgres/UserNotificationEmailRollupMetadataMigration.test.ts +85 -0
- package/Tests/Server/Infrastructure/Postgres/UserNotificationEmailRollupTableMigration.test.ts +34 -3
- package/Tests/Server/Middleware/PasskeyRateLimit.test.ts +141 -0
- package/Tests/Server/Services/AutoImportRuleRearmRecentScans.test.ts +343 -0
- package/Tests/Server/Services/AutoImportRuleWriteRearmsScans.test.ts +381 -0
- package/Tests/Server/Services/BillingPaymentMethodServiceSync.test.ts +175 -0
- package/Tests/Server/Services/BillingService.test.ts +26 -11
- package/Tests/Server/Services/BillingServiceMeteredPaymentAuthorization.test.ts +245 -0
- package/Tests/Server/Services/BillingServicePaymentMethodReads.test.ts +362 -0
- package/Tests/Server/Services/CloudResourceInstanceStalePrune.test.ts +188 -0
- package/Tests/Server/Services/DiscoveryScanClaimHookFreeSafety.test.ts +16 -2
- package/Tests/Server/Services/GoogleSecOpsDiagnosticsModel.test.ts +153 -0
- package/Tests/Server/Services/HookFreeWriteLengthClamp.test.ts +13 -0
- package/Tests/Server/Services/InventoryItemPromotionGate.test.ts +5 -0
- package/Tests/Server/Services/InventoryUpsertSchemaParity.test.ts +36 -0
- package/Tests/Server/Services/InventoryUpsertTruncationGuard.test.ts +151 -0
- package/Tests/Server/Services/MobilePasskeyLoginIntegration.test.ts +231 -0
- package/Tests/Server/Services/MobilePasskeyLoginService.test.ts +315 -0
- package/Tests/Server/Services/MonitorPayAsYouGoBilling.test.ts +302 -0
- package/Tests/Server/Services/MonitorTemplateServiceFieldSync.test.ts +994 -0
- package/Tests/Server/Services/NetworkDeviceAutoImportRuleService.test.ts +22 -0
- package/Tests/Server/Services/OpenTelemetryResourceRetentionMemo.test.ts +7 -1
- package/Tests/Server/Services/PayAsYouGoBillingService.test.ts +857 -0
- package/Tests/Server/Services/PayAsYouGoStripeRateLimitBudget.test.ts +379 -0
- package/Tests/Server/Services/ProjectServiceExtendTrial.test.ts +99 -1
- package/Tests/Server/Services/ProjectServiceSubscriptionSeats.test.ts +134 -0
- package/Tests/Server/Services/ResourceFeedServices.test.ts +3 -2
- package/Tests/Server/Services/ResourceUpdateLastSeenLivenessFallback.test.ts +32 -6
- package/Tests/Server/Services/SecurityEventLastErrorColumnWidth.test.ts +363 -175
- package/Tests/Server/Services/ServerlessFunctionInstanceStalePrune.test.ts +200 -0
- package/Tests/Server/Services/TeamMemberAutoAcceptInvitation.test.ts +3 -0
- package/Tests/Server/Services/TeamMemberBillingSeatSync.test.ts +762 -0
- package/Tests/Server/Services/TeamMemberInviteRegistrationToken.test.ts +3 -0
- package/Tests/Server/Services/TelemetryIngestionKeyPolicyResolution.test.ts +72 -0
- package/Tests/Server/Services/TelemetryIngestionKeyValidation.test.ts +17 -4
- package/Tests/Server/Services/TelemetryPayAsYouGoBilling.test.ts +355 -0
- package/Tests/Server/Services/TraceAnalyticsResourceDisplayNames.test.ts +41 -4
- package/Tests/Server/Services/UserNotificationEmailRollupModels.test.ts +37 -0
- package/Tests/Server/Services/UserWebAuthnPasskeyIntegration.test.ts +401 -0
- package/Tests/Server/Services/UserWebAuthnPasskeyService.test.ts +769 -0
- package/Tests/Server/Services/UserWebAuthnUserVerification.test.ts +649 -0
- package/Tests/Server/Services/VMwareResourceService.test.ts +945 -0
- package/Tests/Server/Services/VMwareVCenterService.test.ts +722 -0
- package/Tests/Server/Services/WorkflowVariableRenameUniqueness.test.ts +842 -0
- package/Tests/Server/TestingUtils/PasskeyHttpIntegration.ts +102 -0
- package/Tests/Server/TestingUtils/SecurityKey.ts +464 -0
- package/Tests/Server/TestingUtils/WebAuthnServiceMocks.ts +60 -0
- package/Tests/Server/Types/Billing/PayAsYouGoMeteredAuthorization.test.ts +573 -0
- package/Tests/Server/Types/Database/Permissions/ProjectCustomerSupportAccessColumn.test.ts +261 -0
- package/Tests/Server/Types/Database/Permissions/WorkflowVariableColumnPermission.test.ts +95 -0
- package/Tests/Server/Utils/CodeRepository/GitHubAgentTaskTrigger.test.ts +2101 -0
- package/Tests/Server/Utils/CodeRepository/GitHubCommandAuthorizer.test.ts +883 -0
- package/Tests/Server/Utils/CodeRepository/GitHubCommandParser.test.ts +1474 -0
- package/Tests/Server/Utils/CodeRepository/GitHubConversation.test.ts +1526 -0
- package/Tests/Server/Utils/CodeRepository/GitHubReplyComposer.test.ts +1389 -0
- package/Tests/Server/Utils/CodeRepository/GitHubRunReply.test.ts +1707 -0
- package/Tests/Server/Utils/CodeRepository/GitHubWebhookHandler.test.ts +2202 -0
- package/Tests/Server/Utils/Dashboard/PublicDashboardResourceListPolicy.test.ts +345 -0
- package/Tests/Server/Utils/DataSource/EgressGuard.test.ts +896 -0
- package/Tests/Server/Utils/EmailRollup/EmailRollupMetadataIntegration.test.ts +246 -0
- package/Tests/Server/Utils/EmailRollup/EmailRollupRenderer.test.ts +299 -1
- package/Tests/Server/Utils/EmailRollup/EmailRollupTestHarness.ts +6 -0
- package/Tests/Server/Utils/EmailRollup/EmailRollupWriter.test.ts +185 -0
- package/Tests/Server/Utils/LogRedaction.test.ts +33 -0
- package/Tests/Server/Utils/Monitor/CapturedMetricAttributeUtil.test.ts +2 -1
- package/Tests/Server/Utils/Monitor/Criteria/TelemetryStepMetricAlias.test.ts +10 -0
- package/Tests/Server/Utils/Monitor/MonitorAlertResourceLinking.test.ts +66 -1
- package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluator.test.ts +362 -0
- package/Tests/Server/Utils/Monitor/MonitorDependencySuppressionCreatorSkip.test.ts +1 -0
- package/Tests/Server/Utils/Monitor/MonitorIncidentResourceLinking.test.ts +60 -1
- package/Tests/Server/Utils/Monitor/MonitorMaintenanceSuppression.test.ts +60 -0
- package/Tests/Server/Utils/Monitor/MonitorRootCauseTargetResolution.test.ts +423 -0
- package/Tests/Server/Utils/Monitor/MonitorStepResourceIdentity.test.ts +6 -0
- package/Tests/Server/Utils/Monitor/MonitorSummaryPersistence.test.ts +1 -0
- package/Tests/Server/Utils/Monitor/PerSeriesSecondEntityAlerting.test.ts +1 -0
- package/Tests/Server/Utils/Monitor/SeriesResourceLinker.test.ts +46 -2
- package/Tests/Server/Utils/SecurityEvent/ConnectorErrorMessage.test.ts +81 -45
- package/Tests/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsRunExecutor.test.ts +711 -0
- package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsClickhouseIntegration.test.ts +223 -0
- package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsClient.test.ts +69 -1
- package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsDiagnostics.test.ts +838 -0
- package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsErrorHandling.test.ts +241 -28
- package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsHttpIntegration.test.ts +651 -0
- package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsPoller.test.ts +11 -0
- package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsPollerFailureTaxonomy.test.ts +13 -12
- package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsPollerHardening.test.ts +112 -127
- package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsRequestContract.test.ts +12 -18
- package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsResponseParsing.test.ts +43 -0
- package/Tests/Server/Utils/Telemetry/TelemetryEntity.test.ts +580 -0
- package/Tests/Server/Utils/Telemetry/VMwareSnapshotScan.test.ts +1748 -0
- package/Tests/Types/Cloud/CloudPlatform.test.ts +574 -0
- package/Tests/Types/Monitor/MetricAndProfileDefaultCriteria.test.ts +2 -0
- package/Tests/Types/Monitor/MonitorCriteriaMetricVariables.test.ts +9 -0
- package/Tests/Types/Monitor/MonitorStepDefaultTelemetryConfig.test.ts +1 -0
- package/Tests/Types/Monitor/MonitorStepInfraMonitorUtils.test.ts +53 -8
- package/Tests/Types/Monitor/MonitorStepInfrastructureMonitors.test.ts +97 -0
- package/Tests/Types/Monitor/MonitorStepMetricViewConfigUtil.test.ts +1 -0
- package/Tests/Types/Monitor/MonitorStepTemplateSyncFields.test.ts +107 -0
- package/Tests/Types/Monitor/MonitorTemplateSyncField.test.ts +172 -0
- package/Tests/Types/Monitor/MonitorType.test.ts +1 -0
- package/Tests/Types/Monitor/MonitorTypeKeywords.test.ts +2 -0
- package/Tests/Types/Monitor/Recommendation/MonitorRecommendationAlertDebuggability.test.ts +1 -0
- package/Tests/Types/Monitor/Recommendation/MonitorRecommendationCatalog.test.ts +9 -0
- package/Tests/Types/Monitor/Recommendation/MonitorRecommendationCoverage.test.ts +1 -1
- package/Tests/Types/Monitor/Recommendation/MonitorRecommendationNotificationMode.test.ts +1 -1
- package/Tests/Types/Monitor/SeriesContext/SeriesDebugHints.test.ts +131 -0
- package/Tests/Types/Monitor/SeriesContext/SeriesLabelDisplay.test.ts +10 -0
- package/Tests/Types/Monitor/VMwareAlertTemplates.test.ts +1608 -0
- package/Tests/Types/Monitor/VMwareMetricCatalog.test.ts +488 -0
- package/Tests/Types/OnCallDutyPolicy/OverrideProvenance.test.ts +295 -0
- package/Tests/Types/Runner/RunnerLiveStatus.test.ts +2 -2
- package/Tests/Types/SignupPassword.test.ts +205 -0
- package/Tests/Types/StatusPage/StatusPageLanguage.test.ts +44 -0
- package/Tests/Types/SupportedLanguages.test.ts +119 -0
- package/Tests/Types/Telemetry/EntityTypeGroups.test.ts +19 -0
- package/Tests/UI/ColorPickerBuild.test.ts +86 -0
- package/Tests/UI/Components/Breadcrumbs.test.tsx +32 -0
- package/Tests/UI/Components/Forms/ModelFormPreservesUserInput.test.tsx +937 -0
- package/Tests/UI/Components/Forms/ModelFormWriteOnlyColumnSelect.test.tsx +230 -0
- package/Tests/UI/Components/Forms/ValidationFormSteps.test.ts +3 -4
- package/Tests/UI/Components/LabelRuleImportExport.test.tsx +907 -0
- package/Tests/UI/Components/ModelTable/ModelTableCreateAfterEdit.test.tsx +514 -0
- package/Tests/UI/Components/MonitorGraphs/Uptime.test.tsx +301 -0
- package/Tests/UI/Components/MonitorTemplateVariables/TemplateVariablesCatalog.test.ts +2 -1
- package/Tests/UI/Components/TooltipLazyContent.test.tsx +373 -0
- package/Tests/UI/MicrosoftTeams/MicrosoftTeamsMessagingEndpointGuidance.test.tsx +43 -12
- package/Tests/UI/ReactDomBuild.test.ts +104 -0
- package/Tests/UI/ReactRouterSingletonBuild.test.ts +11 -1
- package/Tests/UI/Utils/Breadcrumb/fixtures/RealBreadcrumbTrails.ts +148 -0
- package/Tests/UI/Utils/Breadcrumb/fixtures/RealRoutePatterns.ts +29 -0
- package/Tests/UI/Utils/DashboardStackingLayers.test.ts +197 -0
- package/Tests/UI/Utils/LabelRuleImportExport.test.ts +546 -0
- package/Tests/UI/Utils/ModelAPITenantHeader.test.ts +109 -2
- package/Tests/UI/Utils/UseDashboardGridDnd.test.tsx +7 -2
- package/Tests/UI/Utils/UserEmailRegistrationStatus.test.tsx +295 -0
- package/Tests/UI/Utils/WebAuthn.test.ts +265 -0
- package/Tests/Utils/Dashboard/Components/DashboardComponentDefaults.test.ts +5 -0
- package/Tests/Utils/Dashboard/Components/DashboardVMwareListComponents.test.ts +548 -0
- package/Tests/Utils/Dashboard/DashboardResourceListSharedArgs.test.ts +17 -3
- package/Tests/Utils/LabelRuleImportExport.test.ts +583 -0
- package/Tests/Utils/Monitor/MonitorSummarySnapshotUtil.test.ts +1 -0
- package/Tests/Utils/Monitor/MonitorTemplateSyncUtil.test.ts +590 -0
- package/Tests/Utils/RequestFailedDetails.test.ts +325 -0
- package/Tests/Utils/Telemetry/CloudInstanceIdentity.test.ts +255 -0
- package/Tests/Utils/Telemetry/EntityRelationship.test.ts +167 -0
- package/Tests/Utils/WebAuthnTestUtil.ts +79 -0
- package/Tests/__mocks__/simplewebauthn.js +33 -4
- package/Types/AI/CodeFixTaskContext.ts +122 -0
- package/Types/AI/CodeFixTaskType.ts +62 -0
- package/Types/Accounts/AccountsLanguage.ts +1 -0
- package/Types/AdminDashboard/AdminDashboardLanguage.ts +1 -0
- package/Types/Calendar/CalendarEvent.ts +16 -0
- package/Types/Cloud/CloudPlatform.ts +372 -0
- package/Types/CodeRepository/GitHubCommand.ts +69 -0
- package/Types/Dashboard/DashboardComponentType.ts +2 -0
- package/Types/Dashboard/DashboardComponents/ComponentArgument.ts +1 -0
- package/Types/Dashboard/DashboardComponents/DashboardVMwareHostListComponent.ts +23 -0
- package/Types/Dashboard/DashboardComponents/DashboardVMwareVirtualMachineListComponent.ts +30 -0
- package/Types/Dashboard/DashboardLanguage.ts +1 -0
- package/Types/Dashboard/DashboardTemplates.ts +314 -0
- package/Types/Docs/DocsLanguage.ts +1 -0
- package/Types/Exception/EgressGuardException.ts +65 -0
- package/Types/Icon/IconProp.ts +1 -0
- package/Types/Monitor/MetricMonitor/MetricMonitorResponse.ts +36 -0
- package/Types/Monitor/MonitorCriteriaInstance.ts +1 -0
- package/Types/Monitor/MonitorStep.ts +78 -17
- package/Types/Monitor/MonitorStepMetricViewConfigUtil.ts +1 -0
- package/Types/Monitor/MonitorStepVMwareMonitor.ts +127 -0
- package/Types/Monitor/MonitorTemplateSyncField.ts +291 -0
- package/Types/Monitor/MonitorType.ts +24 -0
- package/Types/Monitor/Recommendation/MonitorRecommendationCatalog.ts +33 -0
- package/Types/Monitor/Recommendation/MonitorRecommendationTypes.ts +2 -0
- package/Types/Monitor/Recommendation/MonitorRecommendationUtil.ts +3 -0
- package/Types/Monitor/SeriesContext/SeriesDebugHints.ts +158 -0
- package/Types/Monitor/SeriesContext/SeriesLabelDisplay.ts +61 -0
- package/Types/Monitor/VMwareAlertTemplates.ts +1070 -0
- package/Types/Monitor/VMwareMetricCatalog.ts +914 -0
- package/Types/OnCallDutyPolicy/ScheduleShiftUtil.ts +29 -0
- package/Types/OnCallDutyPolicy/UserOverrideUtil.ts +16 -0
- package/Types/Password.ts +84 -5
- package/Types/Permission.ts +265 -0
- package/Types/Probe/RequestFailedDetails.ts +7 -0
- package/Types/Runner/RunnerLiveStatus.ts +1 -1
- package/Types/SecurityEvent/GoogleSecOpsDiagnostics.ts +56 -0
- package/Types/StatusPage/StatusPageLanguage.ts +1 -0
- package/Types/Telemetry/EntityType.ts +21 -0
- package/Types/Telemetry/ServiceType.ts +1 -0
- package/Typings/Index.d.ts +63 -0
- package/UI/Components/Breadcrumbs/Breadcrumbs.tsx +2 -2
- package/UI/Components/Calendar/Calendar.css +78 -0
- package/UI/Components/Calendar/Calendar.tsx +28 -3
- package/UI/Components/Forms/BasicForm.tsx +44 -2
- package/UI/Components/Forms/Fields/ColorPicker.tsx +2 -1
- package/UI/Components/Forms/Fields/FormField.tsx +16 -3
- package/UI/Components/Forms/ModelForm.tsx +159 -5
- package/UI/Components/Forms/Types/Field.ts +8 -1
- package/UI/Components/Graphs/DayUptimeGraph.tsx +8 -10
- package/UI/Components/Icon/Icon.tsx +16 -0
- package/UI/Components/Input/Input.tsx +42 -7
- package/UI/Components/LabelRule/ImportLabelRulesModal.tsx +441 -0
- package/UI/Components/LabelRule/LabelRuleTable.tsx +142 -0
- package/UI/Components/ModelTable/BaseModelTable.tsx +22 -5
- package/UI/Components/ModelTable/ModelTable.tsx +14 -6
- package/UI/Components/MonitorGraphs/Uptime.tsx +17 -27
- package/UI/Components/MonitorTemplateVariables/TemplateVariablesCatalog.ts +3 -1
- package/UI/Components/Tooltip/Tooltip.tsx +52 -6
- package/UI/Utils/DashboardStackingLayers.ts +85 -0
- package/UI/Utils/LabelRuleImportExport.ts +310 -0
- package/UI/Utils/ModelAPI/ModelAPI.ts +4 -3
- package/UI/Utils/TelemetryService.ts +4 -3
- package/UI/Utils/UseDashboardGridDnd.ts +4 -1
- package/UI/Utils/UserEmailRegistrationStatus.ts +33 -5
- package/UI/Utils/WebAuthn.ts +220 -0
- package/UI/esbuild-config.js +4 -0
- package/Utils/API.ts +34 -0
- package/Utils/Dashboard/Components/DashboardVMwareHostListComponent.ts +38 -0
- package/Utils/Dashboard/Components/DashboardVMwareResourceListShared.ts +70 -0
- package/Utils/Dashboard/Components/DashboardVMwareVirtualMachineListComponent.ts +76 -0
- package/Utils/Dashboard/Components/Index.ts +16 -0
- package/Utils/LabelRuleImportExport.ts +496 -0
- package/Utils/Monitor/MonitorTemplateSyncUtil.ts +164 -0
- package/Utils/NetworkDiscovery/DiscoveryScanStatus.ts +8 -0
- package/Utils/Telemetry/CloudInstanceIdentity.ts +94 -0
- package/Utils/Telemetry/EntityKey.ts +19 -0
- package/Utils/Telemetry/EntityRelationship.ts +19 -0
- package/build/dist/Models/DatabaseModels/Alert.js +54 -0
- package/build/dist/Models/DatabaseModels/Alert.js.map +1 -1
- package/build/dist/Models/DatabaseModels/CloudResource.js +44 -7
- package/build/dist/Models/DatabaseModels/CloudResource.js.map +1 -1
- package/build/dist/Models/DatabaseModels/CodeRepository.js +87 -0
- package/build/dist/Models/DatabaseModels/CodeRepository.js.map +1 -1
- package/build/dist/Models/DatabaseModels/GoogleSecOpsConnection.js +60 -7
- package/build/dist/Models/DatabaseModels/GoogleSecOpsConnection.js.map +1 -1
- package/build/dist/Models/DatabaseModels/GoogleSecOpsConnectionRun.js +287 -0
- package/build/dist/Models/DatabaseModels/GoogleSecOpsConnectionRun.js.map +1 -0
- package/build/dist/Models/DatabaseModels/Incident.js +54 -0
- package/build/dist/Models/DatabaseModels/Incident.js.map +1 -1
- package/build/dist/Models/DatabaseModels/Index.js +16 -0
- package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
- package/build/dist/Models/DatabaseModels/ScheduledMaintenance.js +54 -0
- package/build/dist/Models/DatabaseModels/ScheduledMaintenance.js.map +1 -1
- package/build/dist/Models/DatabaseModels/User.js +57 -4
- package/build/dist/Models/DatabaseModels/User.js.map +1 -1
- package/build/dist/Models/DatabaseModels/UserNotificationEmailRollupItem.js +47 -2
- package/build/dist/Models/DatabaseModels/UserNotificationEmailRollupItem.js.map +1 -1
- package/build/dist/Models/DatabaseModels/UserWebAuthn.js +18 -0
- package/build/dist/Models/DatabaseModels/UserWebAuthn.js.map +1 -1
- package/build/dist/Models/DatabaseModels/VMwareResource.js +1201 -0
- package/build/dist/Models/DatabaseModels/VMwareResource.js.map +1 -0
- package/build/dist/Models/DatabaseModels/VMwareVCenter.js +1149 -0
- package/build/dist/Models/DatabaseModels/VMwareVCenter.js.map +1 -0
- package/build/dist/Models/DatabaseModels/VMwareVCenterFeed.js +648 -0
- package/build/dist/Models/DatabaseModels/VMwareVCenterFeed.js.map +1 -0
- package/build/dist/Models/DatabaseModels/VMwareVCenterLabelRule.js +522 -0
- package/build/dist/Models/DatabaseModels/VMwareVCenterLabelRule.js.map +1 -0
- package/build/dist/Models/DatabaseModels/VMwareVCenterOwnerRule.js +603 -0
- package/build/dist/Models/DatabaseModels/VMwareVCenterOwnerRule.js.map +1 -0
- package/build/dist/Models/DatabaseModels/VMwareVCenterOwnerTeam.js +503 -0
- package/build/dist/Models/DatabaseModels/VMwareVCenterOwnerTeam.js.map +1 -0
- package/build/dist/Models/DatabaseModels/VMwareVCenterOwnerUser.js +502 -0
- package/build/dist/Models/DatabaseModels/VMwareVCenterOwnerUser.js.map +1 -0
- package/build/dist/Models/DatabaseModels/WorkflowVariable.js +22 -1
- package/build/dist/Models/DatabaseModels/WorkflowVariable.js.map +1 -1
- package/build/dist/Server/API/AIAgentDataAPI.js +63 -3
- package/build/dist/Server/API/AIAgentDataAPI.js.map +1 -1
- package/build/dist/Server/API/AIAgentGitHubAPI.js +363 -0
- package/build/dist/Server/API/AIAgentGitHubAPI.js.map +1 -0
- package/build/dist/Server/API/AIChatAPI.js +19 -1
- package/build/dist/Server/API/AIChatAPI.js.map +1 -1
- package/build/dist/Server/API/BillingAPI.js +22 -0
- package/build/dist/Server/API/BillingAPI.js.map +1 -1
- package/build/dist/Server/API/DashboardAPI.js +10 -0
- package/build/dist/Server/API/DashboardAPI.js.map +1 -1
- package/build/dist/Server/API/GitHubAPI.js +33 -5
- package/build/dist/Server/API/GitHubAPI.js.map +1 -1
- package/build/dist/Server/API/GoogleSecOpsConnectionAPI.js +56 -0
- package/build/dist/Server/API/GoogleSecOpsConnectionAPI.js.map +1 -0
- package/build/dist/Server/API/UserWebAuthnAPI.js +17 -1
- package/build/dist/Server/API/UserWebAuthnAPI.js.map +1 -1
- package/build/dist/Server/API/VMwareResourceAPI.js +106 -0
- package/build/dist/Server/API/VMwareResourceAPI.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1791800000000-SplitWebAuthnChallengeByPurpose.js +41 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1791800000000-SplitWebAuthnChallengeByPurpose.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1791900000000-AddSeverityAndStateToNotificationEmailRollup.js +14 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1791900000000-AddSeverityAndStateToNotificationEmailRollup.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1792000000000-AddVMwareTables.js +241 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1792000000000-AddVMwareTables.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1792100000000-AddGitHubCommandSettingsToCodeRepository.js +36 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1792100000000-AddGitHubCommandSettingsToCodeRepository.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1792200000000-AddUserWebAuthnPurpose.js +12 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1792200000000-AddUserWebAuthnPurpose.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1792300000000-AddGoogleSecOpsDiagnostics.js +34 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1792300000000-AddGoogleSecOpsDiagnostics.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +12 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
- package/build/dist/Server/Middleware/IdentityRateLimit.js +80 -0
- package/build/dist/Server/Middleware/IdentityRateLimit.js.map +1 -1
- package/build/dist/Server/Services/AIRunService.js +14 -1
- package/build/dist/Server/Services/AIRunService.js.map +1 -1
- package/build/dist/Server/Services/BillingPaymentMethodService.js +71 -21
- package/build/dist/Server/Services/BillingPaymentMethodService.js.map +1 -1
- package/build/dist/Server/Services/BillingService.js +210 -22
- package/build/dist/Server/Services/BillingService.js.map +1 -1
- package/build/dist/Server/Services/CloudResourceInstanceService.js +66 -0
- package/build/dist/Server/Services/CloudResourceInstanceService.js.map +1 -1
- package/build/dist/Server/Services/ExceptionAggregationService.js +1 -0
- package/build/dist/Server/Services/ExceptionAggregationService.js.map +1 -1
- package/build/dist/Server/Services/GoogleSecOpsConnectionRunService.js +9 -0
- package/build/dist/Server/Services/GoogleSecOpsConnectionRunService.js.map +1 -0
- package/build/dist/Server/Services/GoogleSecOpsConnectionService.js +7 -0
- package/build/dist/Server/Services/GoogleSecOpsConnectionService.js.map +1 -1
- package/build/dist/Server/Services/Index.js +16 -0
- package/build/dist/Server/Services/Index.js.map +1 -1
- package/build/dist/Server/Services/LogAggregationService.js +1 -0
- package/build/dist/Server/Services/LogAggregationService.js.map +1 -1
- package/build/dist/Server/Services/MetricAggregationService.js +1 -0
- package/build/dist/Server/Services/MetricAggregationService.js.map +1 -1
- package/build/dist/Server/Services/MobilePasskeyLoginService.js +137 -0
- package/build/dist/Server/Services/MobilePasskeyLoginService.js.map +1 -0
- package/build/dist/Server/Services/MonitorService.js +34 -1
- package/build/dist/Server/Services/MonitorService.js.map +1 -1
- package/build/dist/Server/Services/MonitorTemplateService.js +64 -26
- package/build/dist/Server/Services/MonitorTemplateService.js.map +1 -1
- package/build/dist/Server/Services/NetworkDeviceAutoImportRuleEngineService.js +124 -1
- package/build/dist/Server/Services/NetworkDeviceAutoImportRuleEngineService.js.map +1 -1
- package/build/dist/Server/Services/NetworkDeviceAutoImportRuleService.js +142 -2
- package/build/dist/Server/Services/NetworkDeviceAutoImportRuleService.js.map +1 -1
- package/build/dist/Server/Services/OpenTelemetryIngestService.js +28 -13
- package/build/dist/Server/Services/OpenTelemetryIngestService.js.map +1 -1
- package/build/dist/Server/Services/PayAsYouGoBillingService.js +297 -0
- package/build/dist/Server/Services/PayAsYouGoBillingService.js.map +1 -0
- package/build/dist/Server/Services/ProjectService.js +38 -2
- package/build/dist/Server/Services/ProjectService.js.map +1 -1
- package/build/dist/Server/Services/ServerlessFunctionInstanceService.js +66 -0
- package/build/dist/Server/Services/ServerlessFunctionInstanceService.js.map +1 -1
- package/build/dist/Server/Services/TeamMemberService.js +85 -28
- package/build/dist/Server/Services/TeamMemberService.js.map +1 -1
- package/build/dist/Server/Services/TelemetryIngestionKeyService.js +61 -10
- package/build/dist/Server/Services/TelemetryIngestionKeyService.js.map +1 -1
- package/build/dist/Server/Services/TelemetryUsageBillingService.js +62 -5
- package/build/dist/Server/Services/TelemetryUsageBillingService.js.map +1 -1
- package/build/dist/Server/Services/TraceAggregationService.js +5 -4
- package/build/dist/Server/Services/TraceAggregationService.js.map +1 -1
- package/build/dist/Server/Services/UserWebAuthnService.js +345 -113
- package/build/dist/Server/Services/UserWebAuthnService.js.map +1 -1
- package/build/dist/Server/Services/VMwareResourceService.js +395 -0
- package/build/dist/Server/Services/VMwareResourceService.js.map +1 -0
- package/build/dist/Server/Services/VMwareVCenterFeedService.js +89 -0
- package/build/dist/Server/Services/VMwareVCenterFeedService.js.map +1 -0
- package/build/dist/Server/Services/VMwareVCenterLabelRuleEngineService.js +197 -0
- package/build/dist/Server/Services/VMwareVCenterLabelRuleEngineService.js.map +1 -0
- package/build/dist/Server/Services/VMwareVCenterLabelRuleService.js +13 -0
- package/build/dist/Server/Services/VMwareVCenterLabelRuleService.js.map +1 -0
- package/build/dist/Server/Services/VMwareVCenterOwnerRuleEngineService.js +214 -0
- package/build/dist/Server/Services/VMwareVCenterOwnerRuleEngineService.js.map +1 -0
- package/build/dist/Server/Services/VMwareVCenterOwnerRuleService.js +13 -0
- package/build/dist/Server/Services/VMwareVCenterOwnerRuleService.js.map +1 -0
- package/build/dist/Server/Services/VMwareVCenterOwnerTeamService.js +129 -0
- package/build/dist/Server/Services/VMwareVCenterOwnerTeamService.js.map +1 -0
- package/build/dist/Server/Services/VMwareVCenterOwnerUserService.js +122 -0
- package/build/dist/Server/Services/VMwareVCenterOwnerUserService.js.map +1 -0
- package/build/dist/Server/Services/VMwareVCenterService.js +533 -0
- package/build/dist/Server/Services/VMwareVCenterService.js.map +1 -0
- package/build/dist/Server/Services/WorkflowVariableService.js +169 -0
- package/build/dist/Server/Services/WorkflowVariableService.js.map +1 -1
- package/build/dist/Server/Types/Billing/MeteredPlan/ActiveMonitoringMeteredPlan.js +12 -1
- package/build/dist/Server/Types/Billing/MeteredPlan/ActiveMonitoringMeteredPlan.js.map +1 -1
- package/build/dist/Server/Types/Billing/MeteredPlan/TelemetryMeteredPlan.js +25 -2
- package/build/dist/Server/Types/Billing/MeteredPlan/TelemetryMeteredPlan.js.map +1 -1
- package/build/dist/Server/Utils/CodeRepository/GitHub/GitHub.js.map +1 -1
- package/build/dist/Server/Utils/CodeRepository/GitHub/GitHubAgentTaskTrigger.js +275 -0
- package/build/dist/Server/Utils/CodeRepository/GitHub/GitHubAgentTaskTrigger.js.map +1 -0
- package/build/dist/Server/Utils/CodeRepository/GitHub/GitHubCommandAuthorizer.js +226 -0
- package/build/dist/Server/Utils/CodeRepository/GitHub/GitHubCommandAuthorizer.js.map +1 -0
- package/build/dist/Server/Utils/CodeRepository/GitHub/GitHubCommandParser.js +297 -0
- package/build/dist/Server/Utils/CodeRepository/GitHub/GitHubCommandParser.js.map +1 -0
- package/build/dist/Server/Utils/CodeRepository/GitHub/GitHubConversation.js +582 -0
- package/build/dist/Server/Utils/CodeRepository/GitHub/GitHubConversation.js.map +1 -0
- package/build/dist/Server/Utils/CodeRepository/GitHub/GitHubReplyComposer.js +275 -0
- package/build/dist/Server/Utils/CodeRepository/GitHub/GitHubReplyComposer.js.map +1 -0
- package/build/dist/Server/Utils/CodeRepository/GitHub/GitHubRunReply.js +365 -0
- package/build/dist/Server/Utils/CodeRepository/GitHub/GitHubRunReply.js.map +1 -0
- package/build/dist/Server/Utils/CodeRepository/GitHub/GitHubWebhookEvents.js +103 -0
- package/build/dist/Server/Utils/CodeRepository/GitHub/GitHubWebhookEvents.js.map +1 -0
- package/build/dist/Server/Utils/CodeRepository/GitHub/GitHubWebhookHandler.js +661 -0
- package/build/dist/Server/Utils/CodeRepository/GitHub/GitHubWebhookHandler.js.map +1 -0
- package/build/dist/Server/Utils/Dashboard/PublicDashboardResourceListPolicy.js +113 -0
- package/build/dist/Server/Utils/Dashboard/PublicDashboardResourceListPolicy.js.map +1 -1
- package/build/dist/Server/Utils/DataSource/EgressGuard.js +99 -15
- package/build/dist/Server/Utils/DataSource/EgressGuard.js.map +1 -1
- package/build/dist/Server/Utils/EmailRollup/EmailRollupFlushRunner.js +2 -0
- package/build/dist/Server/Utils/EmailRollup/EmailRollupFlushRunner.js.map +1 -1
- package/build/dist/Server/Utils/EmailRollup/EmailRollupRenderer.js +18 -0
- package/build/dist/Server/Utils/EmailRollup/EmailRollupRenderer.js.map +1 -1
- package/build/dist/Server/Utils/EmailRollup/EmailRollupWriter.js +31 -2
- package/build/dist/Server/Utils/EmailRollup/EmailRollupWriter.js.map +1 -1
- package/build/dist/Server/Utils/LogRedaction.js +2 -1
- package/build/dist/Server/Utils/LogRedaction.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorAlert.js +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js +219 -4
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorMaintenanceSuppression.js +12 -4
- package/build/dist/Server/Utils/Monitor/MonitorMaintenanceSuppression.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorResourceContext.js +5 -3
- package/build/dist/Server/Utils/Monitor/MonitorResourceContext.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorStepResourceIdentity.js +22 -9
- package/build/dist/Server/Utils/Monitor/MonitorStepResourceIdentity.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorTemplateUtil.js +1 -0
- package/build/dist/Server/Utils/Monitor/MonitorTemplateUtil.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/SeriesResourceLabels.js +21 -0
- package/build/dist/Server/Utils/Monitor/SeriesResourceLabels.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/SeriesResourceLinker.js +21 -6
- package/build/dist/Server/Utils/Monitor/SeriesResourceLinker.js.map +1 -1
- package/build/dist/Server/Utils/ResourceFeed/ResourceFeedUtil.js +3 -3
- package/build/dist/Server/Utils/SecurityEvent/ConnectorErrorMessage.js +9 -9
- package/build/dist/Server/Utils/SecurityEvent/ConnectorErrorMessage.js.map +1 -1
- package/build/dist/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsClient.js +92 -71
- package/build/dist/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsClient.js.map +1 -1
- package/build/dist/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsPoller.js +428 -143
- package/build/dist/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsPoller.js.map +1 -1
- package/build/dist/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsRunExecutor.js +376 -0
- package/build/dist/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsRunExecutor.js.map +1 -0
- package/build/dist/Server/Utils/Telemetry/EntityRegistry.js +5 -0
- package/build/dist/Server/Utils/Telemetry/EntityRegistry.js.map +1 -1
- package/build/dist/Server/Utils/Telemetry/ResourceHeartbeat.js.map +1 -1
- package/build/dist/Server/Utils/Telemetry/TelemetryEntity.js +136 -1
- package/build/dist/Server/Utils/Telemetry/TelemetryEntity.js.map +1 -1
- package/build/dist/Server/Utils/Telemetry/VMwareSnapshotScan.js +933 -0
- package/build/dist/Server/Utils/Telemetry/VMwareSnapshotScan.js.map +1 -0
- package/build/dist/Types/AI/CodeFixTaskContext.js +40 -0
- package/build/dist/Types/AI/CodeFixTaskContext.js.map +1 -1
- package/build/dist/Types/AI/CodeFixTaskType.js +57 -0
- package/build/dist/Types/AI/CodeFixTaskType.js.map +1 -1
- package/build/dist/Types/Accounts/AccountsLanguage.js +1 -0
- package/build/dist/Types/Accounts/AccountsLanguage.js.map +1 -1
- package/build/dist/Types/AdminDashboard/AdminDashboardLanguage.js +1 -0
- package/build/dist/Types/AdminDashboard/AdminDashboardLanguage.js.map +1 -1
- package/build/dist/Types/Cloud/CloudPlatform.js +277 -0
- package/build/dist/Types/Cloud/CloudPlatform.js.map +1 -0
- package/build/dist/Types/CodeRepository/GitHubCommand.js +52 -0
- package/build/dist/Types/CodeRepository/GitHubCommand.js.map +1 -0
- package/build/dist/Types/Dashboard/DashboardComponentType.js +2 -0
- package/build/dist/Types/Dashboard/DashboardComponentType.js.map +1 -1
- package/build/dist/Types/Dashboard/DashboardComponents/ComponentArgument.js +1 -0
- package/build/dist/Types/Dashboard/DashboardComponents/ComponentArgument.js.map +1 -1
- package/build/dist/Types/Dashboard/DashboardComponents/DashboardVMwareHostListComponent.js +2 -0
- package/build/dist/Types/Dashboard/DashboardComponents/DashboardVMwareHostListComponent.js.map +1 -0
- package/build/dist/Types/Dashboard/DashboardComponents/DashboardVMwareVirtualMachineListComponent.js +2 -0
- package/build/dist/Types/Dashboard/DashboardComponents/DashboardVMwareVirtualMachineListComponent.js.map +1 -0
- package/build/dist/Types/Dashboard/DashboardLanguage.js +1 -0
- package/build/dist/Types/Dashboard/DashboardLanguage.js.map +1 -1
- package/build/dist/Types/Dashboard/DashboardTemplates.js +286 -0
- package/build/dist/Types/Dashboard/DashboardTemplates.js.map +1 -1
- package/build/dist/Types/Docs/DocsLanguage.js +1 -0
- package/build/dist/Types/Docs/DocsLanguage.js.map +1 -1
- package/build/dist/Types/Exception/EgressGuardException.js +59 -0
- package/build/dist/Types/Exception/EgressGuardException.js.map +1 -0
- package/build/dist/Types/Icon/IconProp.js +1 -0
- package/build/dist/Types/Icon/IconProp.js.map +1 -1
- package/build/dist/Types/Monitor/MonitorCriteriaInstance.js +1 -0
- package/build/dist/Types/Monitor/MonitorCriteriaInstance.js.map +1 -1
- package/build/dist/Types/Monitor/MonitorStep.js +61 -18
- package/build/dist/Types/Monitor/MonitorStep.js.map +1 -1
- package/build/dist/Types/Monitor/MonitorStepMetricViewConfigUtil.js +1 -0
- package/build/dist/Types/Monitor/MonitorStepMetricViewConfigUtil.js.map +1 -1
- package/build/dist/Types/Monitor/MonitorStepVMwareMonitor.js +71 -0
- package/build/dist/Types/Monitor/MonitorStepVMwareMonitor.js.map +1 -0
- package/build/dist/Types/Monitor/MonitorTemplateSyncField.js +211 -0
- package/build/dist/Types/Monitor/MonitorTemplateSyncField.js.map +1 -0
- package/build/dist/Types/Monitor/MonitorType.js +23 -0
- package/build/dist/Types/Monitor/MonitorType.js.map +1 -1
- package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationCatalog.js +28 -0
- package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationCatalog.js.map +1 -1
- package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationTypes.js +1 -0
- package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationTypes.js.map +1 -1
- package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationUtil.js +2 -0
- package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationUtil.js.map +1 -1
- package/build/dist/Types/Monitor/SeriesContext/SeriesDebugHints.js +120 -0
- package/build/dist/Types/Monitor/SeriesContext/SeriesDebugHints.js.map +1 -1
- package/build/dist/Types/Monitor/SeriesContext/SeriesLabelDisplay.js +60 -0
- package/build/dist/Types/Monitor/SeriesContext/SeriesLabelDisplay.js.map +1 -1
- package/build/dist/Types/Monitor/VMwareAlertTemplates.js +775 -0
- package/build/dist/Types/Monitor/VMwareAlertTemplates.js.map +1 -0
- package/build/dist/Types/Monitor/VMwareMetricCatalog.js +799 -0
- package/build/dist/Types/Monitor/VMwareMetricCatalog.js.map +1 -0
- package/build/dist/Types/OnCallDutyPolicy/ScheduleShiftUtil.js +25 -0
- package/build/dist/Types/OnCallDutyPolicy/ScheduleShiftUtil.js.map +1 -1
- package/build/dist/Types/OnCallDutyPolicy/UserOverrideUtil.js +5 -0
- package/build/dist/Types/OnCallDutyPolicy/UserOverrideUtil.js.map +1 -1
- package/build/dist/Types/Password.js +63 -5
- package/build/dist/Types/Password.js.map +1 -1
- package/build/dist/Types/Permission.js +234 -0
- package/build/dist/Types/Permission.js.map +1 -1
- package/build/dist/Types/Probe/RequestFailedDetails.js +7 -0
- package/build/dist/Types/Probe/RequestFailedDetails.js.map +1 -1
- package/build/dist/Types/SecurityEvent/GoogleSecOpsDiagnostics.js +2 -0
- package/build/dist/Types/SecurityEvent/GoogleSecOpsDiagnostics.js.map +1 -0
- package/build/dist/Types/StatusPage/StatusPageLanguage.js +1 -0
- package/build/dist/Types/StatusPage/StatusPageLanguage.js.map +1 -1
- package/build/dist/Types/Telemetry/EntityType.js +21 -0
- package/build/dist/Types/Telemetry/EntityType.js.map +1 -1
- package/build/dist/Types/Telemetry/ServiceType.js +1 -0
- package/build/dist/Types/Telemetry/ServiceType.js.map +1 -1
- package/build/dist/UI/Components/Breadcrumbs/Breadcrumbs.js +2 -2
- package/build/dist/UI/Components/Breadcrumbs/Breadcrumbs.js.map +1 -1
- package/build/dist/UI/Components/Calendar/Calendar.js +24 -4
- package/build/dist/UI/Components/Calendar/Calendar.js.map +1 -1
- package/build/dist/UI/Components/Forms/BasicForm.js +28 -3
- package/build/dist/UI/Components/Forms/BasicForm.js.map +1 -1
- package/build/dist/UI/Components/Forms/Fields/ColorPicker.js +1 -1
- package/build/dist/UI/Components/Forms/Fields/ColorPicker.js.map +1 -1
- package/build/dist/UI/Components/Forms/Fields/FormField.js +10 -3
- package/build/dist/UI/Components/Forms/Fields/FormField.js.map +1 -1
- package/build/dist/UI/Components/Forms/ModelForm.js +89 -6
- package/build/dist/UI/Components/Forms/ModelForm.js.map +1 -1
- package/build/dist/UI/Components/Graphs/DayUptimeGraph.js +5 -6
- package/build/dist/UI/Components/Graphs/DayUptimeGraph.js.map +1 -1
- package/build/dist/UI/Components/Icon/Icon.js +14 -0
- package/build/dist/UI/Components/Icon/Icon.js.map +1 -1
- package/build/dist/UI/Components/Input/Input.js +37 -8
- package/build/dist/UI/Components/Input/Input.js.map +1 -1
- package/build/dist/UI/Components/LabelRule/ImportLabelRulesModal.js +228 -0
- package/build/dist/UI/Components/LabelRule/ImportLabelRulesModal.js.map +1 -0
- package/build/dist/UI/Components/LabelRule/LabelRuleTable.js +102 -0
- package/build/dist/UI/Components/LabelRule/LabelRuleTable.js.map +1 -0
- package/build/dist/UI/Components/ModelTable/BaseModelTable.js +15 -2
- package/build/dist/UI/Components/ModelTable/BaseModelTable.js.map +1 -1
- package/build/dist/UI/Components/ModelTable/ModelTable.js +13 -5
- package/build/dist/UI/Components/ModelTable/ModelTable.js.map +1 -1
- package/build/dist/UI/Components/MonitorGraphs/Uptime.js +14 -15
- package/build/dist/UI/Components/MonitorGraphs/Uptime.js.map +1 -1
- package/build/dist/UI/Components/MonitorTemplateVariables/TemplateVariablesCatalog.js +2 -0
- package/build/dist/UI/Components/MonitorTemplateVariables/TemplateVariablesCatalog.js.map +1 -1
- package/build/dist/UI/Components/Tooltip/Tooltip.js +33 -4
- package/build/dist/UI/Components/Tooltip/Tooltip.js.map +1 -1
- package/build/dist/UI/Utils/DashboardStackingLayers.js +44 -0
- package/build/dist/UI/Utils/DashboardStackingLayers.js.map +1 -0
- package/build/dist/UI/Utils/LabelRuleImportExport.js +195 -0
- package/build/dist/UI/Utils/LabelRuleImportExport.js.map +1 -0
- package/build/dist/UI/Utils/ModelAPI/ModelAPI.js +4 -4
- package/build/dist/UI/Utils/ModelAPI/ModelAPI.js.map +1 -1
- package/build/dist/UI/Utils/TelemetryService.js +3 -2
- package/build/dist/UI/Utils/TelemetryService.js.map +1 -1
- package/build/dist/UI/Utils/UseDashboardGridDnd.js +2 -1
- package/build/dist/UI/Utils/UseDashboardGridDnd.js.map +1 -1
- package/build/dist/UI/Utils/UserEmailRegistrationStatus.js +21 -4
- package/build/dist/UI/Utils/UserEmailRegistrationStatus.js.map +1 -1
- package/build/dist/UI/Utils/WebAuthn.js +121 -0
- package/build/dist/UI/Utils/WebAuthn.js.map +1 -0
- package/build/dist/Utils/API.js +29 -0
- package/build/dist/Utils/API.js.map +1 -1
- package/build/dist/Utils/Dashboard/Components/DashboardVMwareHostListComponent.js +33 -0
- package/build/dist/Utils/Dashboard/Components/DashboardVMwareHostListComponent.js.map +1 -0
- package/build/dist/Utils/Dashboard/Components/DashboardVMwareResourceListShared.js +54 -0
- package/build/dist/Utils/Dashboard/Components/DashboardVMwareResourceListShared.js.map +1 -0
- package/build/dist/Utils/Dashboard/Components/DashboardVMwareVirtualMachineListComponent.js +60 -0
- package/build/dist/Utils/Dashboard/Components/DashboardVMwareVirtualMachineListComponent.js.map +1 -0
- package/build/dist/Utils/Dashboard/Components/Index.js +8 -0
- package/build/dist/Utils/Dashboard/Components/Index.js.map +1 -1
- package/build/dist/Utils/LabelRuleImportExport.js +356 -0
- package/build/dist/Utils/LabelRuleImportExport.js.map +1 -0
- package/build/dist/Utils/Monitor/MonitorTemplateSyncUtil.js +110 -0
- package/build/dist/Utils/Monitor/MonitorTemplateSyncUtil.js.map +1 -0
- package/build/dist/Utils/NetworkDiscovery/DiscoveryScanStatus.js +6 -0
- package/build/dist/Utils/NetworkDiscovery/DiscoveryScanStatus.js.map +1 -1
- package/build/dist/Utils/Telemetry/CloudInstanceIdentity.js +88 -0
- package/build/dist/Utils/Telemetry/CloudInstanceIdentity.js.map +1 -0
- package/build/dist/Utils/Telemetry/EntityKey.js +15 -0
- package/build/dist/Utils/Telemetry/EntityKey.js.map +1 -1
- package/build/dist/Utils/Telemetry/EntityRelationship.js +13 -0
- package/build/dist/Utils/Telemetry/EntityRelationship.js.map +1 -1
- 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
|
+
});
|