@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,1707 @@
|
|
|
1
|
+
import GitHubRunReply from "../../../../Server/Utils/CodeRepository/GitHub/GitHubRunReply";
|
|
2
|
+
import GitHubConversation, {
|
|
3
|
+
GitHubIssueComment,
|
|
4
|
+
GitHubReaction,
|
|
5
|
+
} from "../../../../Server/Utils/CodeRepository/GitHub/GitHubConversation";
|
|
6
|
+
import AIRunService from "../../../../Server/Services/AIRunService";
|
|
7
|
+
import AIAgentTaskPullRequestService from "../../../../Server/Services/AIAgentTaskPullRequestService";
|
|
8
|
+
import AIRun from "../../../../Models/DatabaseModels/AIRun";
|
|
9
|
+
import AIAgentTaskPullRequest from "../../../../Models/DatabaseModels/AIAgentTaskPullRequest";
|
|
10
|
+
import AIRunStatus from "../../../../Types/AI/AIRunStatus";
|
|
11
|
+
import GitHubCommandType from "../../../../Types/CodeRepository/GitHubCommand";
|
|
12
|
+
import CodeFixTaskContext, {
|
|
13
|
+
GitHubTaskContext,
|
|
14
|
+
} from "../../../../Types/AI/CodeFixTaskContext";
|
|
15
|
+
import ObjectID from "../../../../Types/ObjectID";
|
|
16
|
+
import URL from "../../../../Types/API/URL";
|
|
17
|
+
import LIMIT_MAX from "../../../../Types/Database/LimitMax";
|
|
18
|
+
import logger from "../../../../Server/Utils/Logger";
|
|
19
|
+
import { DashboardClientUrl } from "../../../../Server/EnvironmentConfig";
|
|
20
|
+
import { afterEach, beforeEach, describe, expect, test } from "@jest/globals";
|
|
21
|
+
|
|
22
|
+
/*
|
|
23
|
+
* ---------------------------------------------------------------------------
|
|
24
|
+
* How a GitHub-triggered agent run talks back to the thread it came from.
|
|
25
|
+
*
|
|
26
|
+
* The invariant these tests pin is a counting one: ONE command produces
|
|
27
|
+
* exactly ONE comment from this app. It is acknowledged when the run is
|
|
28
|
+
* accepted and EDITED when the run finishes. A long-running agent that
|
|
29
|
+
* appends a comment per state change turns a pull request into a status log,
|
|
30
|
+
* and a reviewer stops reading it — so "did we edit or did we post again?" is
|
|
31
|
+
* the single most valuable thing to hold still here.
|
|
32
|
+
*
|
|
33
|
+
* Three more properties matter as much and are easier to break by accident:
|
|
34
|
+
*
|
|
35
|
+
* 1. `acknowledge` is BEST EFFORT. The run is already queued by the time it
|
|
36
|
+
* is called, so a comment GitHub refused must not fail the command — it
|
|
37
|
+
* returns null and the work still happens.
|
|
38
|
+
*
|
|
39
|
+
* 2. `reportOutcome` returns a BOOLEAN, and the sweeper writes
|
|
40
|
+
* `reportedToGitHubAt` only on true. That pair is the whole retry story:
|
|
41
|
+
* a false that gets marked anyway silently loses the user's answer
|
|
42
|
+
* forever, and a true that never gets marked posts the answer again
|
|
43
|
+
* every minute for a day. Several tests here drive the sweeper's own
|
|
44
|
+
* `if (posted) markReported(...)` logic to pin both halves at once.
|
|
45
|
+
*
|
|
46
|
+
* 3. The words per status are load-bearing. NoFixFound must read as a
|
|
47
|
+
* FINDING, not a failure — a user who reads "failed" retries verbatim,
|
|
48
|
+
* a user who reads "found nothing" writes a better instruction. And a
|
|
49
|
+
* Revise run must NOT link the pull request the reader is already on.
|
|
50
|
+
*
|
|
51
|
+
* Everything GitHub-facing is a spy: no network, no database, no Postgres.
|
|
52
|
+
* ---------------------------------------------------------------------------
|
|
53
|
+
*/
|
|
54
|
+
|
|
55
|
+
// The GitHub write calls, as this module makes them.
|
|
56
|
+
interface CreateCommentCall {
|
|
57
|
+
installationId: string;
|
|
58
|
+
organizationName: string;
|
|
59
|
+
repositoryName: string;
|
|
60
|
+
issueNumber: number;
|
|
61
|
+
body: string;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
interface UpdateCommentCall {
|
|
65
|
+
installationId: string;
|
|
66
|
+
organizationName: string;
|
|
67
|
+
repositoryName: string;
|
|
68
|
+
commentId: number;
|
|
69
|
+
body: string;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
interface ReactionOnCommentCall {
|
|
73
|
+
installationId: string;
|
|
74
|
+
organizationName: string;
|
|
75
|
+
repositoryName: string;
|
|
76
|
+
commentId: number;
|
|
77
|
+
reaction: GitHubReaction;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
interface ReactionOnIssueCall {
|
|
81
|
+
installationId: string;
|
|
82
|
+
organizationName: string;
|
|
83
|
+
repositoryName: string;
|
|
84
|
+
issueNumber: number;
|
|
85
|
+
reaction: GitHubReaction;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
interface PullRequestFindByCall {
|
|
89
|
+
query: { aiRunId?: ObjectID };
|
|
90
|
+
select: { pullRequestUrl?: boolean; pullRequestNumber?: boolean };
|
|
91
|
+
limit: number;
|
|
92
|
+
skip: number;
|
|
93
|
+
props: { isRoot?: boolean };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
interface UpdateOneByIdCall {
|
|
97
|
+
id: ObjectID;
|
|
98
|
+
data: { taskContext?: { github?: GitHubTaskContext } };
|
|
99
|
+
props: { isRoot?: boolean };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const CODE_REPOSITORY_ID: string = "6650f0b1a1b2c3d4e5f60718";
|
|
103
|
+
const ACKNOWLEDGEMENT_COMMENT_ID: number = 555;
|
|
104
|
+
const TRIGGER_COMMENT_ID: number = 991;
|
|
105
|
+
const ISSUE_NUMBER: number = 42;
|
|
106
|
+
const PULL_REQUEST_NUMBER: number = 7;
|
|
107
|
+
|
|
108
|
+
describe("GitHubRunReply", () => {
|
|
109
|
+
let projectId: ObjectID;
|
|
110
|
+
let aiRunId: ObjectID;
|
|
111
|
+
|
|
112
|
+
let createIssueCommentSpy: jest.SpyInstance;
|
|
113
|
+
let updateIssueCommentSpy: jest.SpyInstance;
|
|
114
|
+
let addReactionToCommentSpy: jest.SpyInstance;
|
|
115
|
+
let addReactionToIssueSpy: jest.SpyInstance;
|
|
116
|
+
let updateOneByIdSpy: jest.SpyInstance;
|
|
117
|
+
let findPullRequestsSpy: jest.SpyInstance;
|
|
118
|
+
let loggerErrorSpy: jest.SpyInstance;
|
|
119
|
+
let loggerWarnSpy: jest.SpyInstance;
|
|
120
|
+
|
|
121
|
+
beforeEach(() => {
|
|
122
|
+
projectId = ObjectID.generate();
|
|
123
|
+
aiRunId = ObjectID.generate();
|
|
124
|
+
|
|
125
|
+
createIssueCommentSpy = jest
|
|
126
|
+
.spyOn(GitHubConversation, "createIssueComment")
|
|
127
|
+
.mockResolvedValue(issueComment(ACKNOWLEDGEMENT_COMMENT_ID));
|
|
128
|
+
|
|
129
|
+
updateIssueCommentSpy = jest
|
|
130
|
+
.spyOn(GitHubConversation, "updateIssueComment")
|
|
131
|
+
.mockResolvedValue(issueComment(ACKNOWLEDGEMENT_COMMENT_ID));
|
|
132
|
+
|
|
133
|
+
/*
|
|
134
|
+
* The real reaction helpers swallow their own failures and answer false —
|
|
135
|
+
* they never reject. Mocking them the same way is what lets the "a failed
|
|
136
|
+
* reaction changes nothing" tests below be honest rather than circular.
|
|
137
|
+
*/
|
|
138
|
+
addReactionToCommentSpy = jest
|
|
139
|
+
.spyOn(GitHubConversation, "addReactionToComment")
|
|
140
|
+
.mockResolvedValue(true);
|
|
141
|
+
|
|
142
|
+
addReactionToIssueSpy = jest
|
|
143
|
+
.spyOn(GitHubConversation, "addReactionToIssue")
|
|
144
|
+
.mockResolvedValue(true);
|
|
145
|
+
|
|
146
|
+
// updateOneById answers with the number of rows it touched.
|
|
147
|
+
updateOneByIdSpy = jest
|
|
148
|
+
.spyOn(AIRunService, "updateOneById")
|
|
149
|
+
.mockResolvedValue(1);
|
|
150
|
+
|
|
151
|
+
findPullRequestsSpy = jest
|
|
152
|
+
.spyOn(AIAgentTaskPullRequestService, "findBy")
|
|
153
|
+
.mockResolvedValue([]);
|
|
154
|
+
|
|
155
|
+
loggerErrorSpy = jest
|
|
156
|
+
.spyOn(logger, "error")
|
|
157
|
+
.mockImplementation((): void => {
|
|
158
|
+
// Swallowed: the failure paths are asserted through the spy itself.
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
loggerWarnSpy = jest.spyOn(logger, "warn").mockImplementation((): void => {
|
|
162
|
+
// Swallowed.
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
afterEach(() => {
|
|
167
|
+
jest.restoreAllMocks();
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
// ---------------------------------------------------------------- builders
|
|
171
|
+
|
|
172
|
+
function issueComment(commentId: number): GitHubIssueComment {
|
|
173
|
+
return {
|
|
174
|
+
commentId: commentId,
|
|
175
|
+
htmlUrl: `https://github.com/acme/checkout/issues/${ISSUE_NUMBER}#issuecomment-${commentId}`,
|
|
176
|
+
body: "",
|
|
177
|
+
authorLogin: "oneuptime[bot]",
|
|
178
|
+
isBot: true,
|
|
179
|
+
createdAt: "2026-09-10T09:00:00.000Z",
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// A run started from an ISSUE — the "@mention implement this" recipe.
|
|
184
|
+
function issueContext(
|
|
185
|
+
overrides: Partial<GitHubTaskContext> = {},
|
|
186
|
+
): GitHubTaskContext {
|
|
187
|
+
return {
|
|
188
|
+
codeRepositoryId: CODE_REPOSITORY_ID,
|
|
189
|
+
installationId: "12345678",
|
|
190
|
+
organizationName: "acme",
|
|
191
|
+
repositoryName: "checkout",
|
|
192
|
+
commandType: GitHubCommandType.Implement,
|
|
193
|
+
instruction: "implement this",
|
|
194
|
+
issueNumber: ISSUE_NUMBER,
|
|
195
|
+
triggerCommentId: TRIGGER_COMMENT_ID,
|
|
196
|
+
...overrides,
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// A run started from a PULL REQUEST — the review / revise recipes.
|
|
201
|
+
function pullRequestContext(
|
|
202
|
+
overrides: Partial<GitHubTaskContext> = {},
|
|
203
|
+
): GitHubTaskContext {
|
|
204
|
+
return {
|
|
205
|
+
codeRepositoryId: CODE_REPOSITORY_ID,
|
|
206
|
+
installationId: "12345678",
|
|
207
|
+
organizationName: "acme",
|
|
208
|
+
repositoryName: "checkout",
|
|
209
|
+
commandType: GitHubCommandType.Revise,
|
|
210
|
+
instruction: "revise this — the retry loop should back off exponentially",
|
|
211
|
+
pullRequestNumber: PULL_REQUEST_NUMBER,
|
|
212
|
+
pullRequestHeadRefName: "oneuptime-ai/retry-backoff",
|
|
213
|
+
triggerCommentId: TRIGGER_COMMENT_ID,
|
|
214
|
+
...overrides,
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function aiRun(data: {
|
|
219
|
+
status: AIRunStatus;
|
|
220
|
+
errorMessage?: string;
|
|
221
|
+
github?: GitHubTaskContext;
|
|
222
|
+
}): AIRun {
|
|
223
|
+
const run: AIRun = new AIRun();
|
|
224
|
+
|
|
225
|
+
run.id = aiRunId;
|
|
226
|
+
run.projectId = projectId;
|
|
227
|
+
run.status = data.status;
|
|
228
|
+
|
|
229
|
+
if (data.errorMessage !== undefined) {
|
|
230
|
+
run.errorMessage = data.errorMessage;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (data.github) {
|
|
234
|
+
run.taskContext = { github: data.github } as CodeFixTaskContext;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return run;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function pullRequestRow(data: {
|
|
241
|
+
pullRequestNumber?: number;
|
|
242
|
+
url?: string;
|
|
243
|
+
}): AIAgentTaskPullRequest {
|
|
244
|
+
const row: AIAgentTaskPullRequest = new AIAgentTaskPullRequest();
|
|
245
|
+
|
|
246
|
+
if (data.pullRequestNumber !== undefined) {
|
|
247
|
+
row.pullRequestNumber = data.pullRequestNumber;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
if (data.url !== undefined) {
|
|
251
|
+
row.pullRequestUrl = URL.fromString(data.url);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
return row;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// ----------------------------------------------------------- call readers
|
|
258
|
+
|
|
259
|
+
function nthCallArgument(spy: jest.SpyInstance, callNumber: number): unknown {
|
|
260
|
+
const call: Array<unknown> | undefined = spy.mock.calls[callNumber - 1] as
|
|
261
|
+
| Array<unknown>
|
|
262
|
+
| undefined;
|
|
263
|
+
|
|
264
|
+
if (!call || call[0] === undefined) {
|
|
265
|
+
throw new Error(`The spy was not called ${callNumber} time(s)`);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return call[0];
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function createdComment(callNumber: number = 1): CreateCommentCall {
|
|
272
|
+
return nthCallArgument(
|
|
273
|
+
createIssueCommentSpy,
|
|
274
|
+
callNumber,
|
|
275
|
+
) as CreateCommentCall;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function updatedComment(callNumber: number = 1): UpdateCommentCall {
|
|
279
|
+
return nthCallArgument(
|
|
280
|
+
updateIssueCommentSpy,
|
|
281
|
+
callNumber,
|
|
282
|
+
) as UpdateCommentCall;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function reactionOnComment(callNumber: number = 1): ReactionOnCommentCall {
|
|
286
|
+
return nthCallArgument(
|
|
287
|
+
addReactionToCommentSpy,
|
|
288
|
+
callNumber,
|
|
289
|
+
) as ReactionOnCommentCall;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function reactionOnIssue(callNumber: number = 1): ReactionOnIssueCall {
|
|
293
|
+
return nthCallArgument(
|
|
294
|
+
addReactionToIssueSpy,
|
|
295
|
+
callNumber,
|
|
296
|
+
) as ReactionOnIssueCall;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/*
|
|
300
|
+
* The body that actually reached GitHub, whichever write carried it. The
|
|
301
|
+
* caller should not have to know whether this run had an acknowledgement to
|
|
302
|
+
* edit — that is exactly what the tests around it are for.
|
|
303
|
+
*/
|
|
304
|
+
async function postedOutcomeBody(data: {
|
|
305
|
+
status: AIRunStatus;
|
|
306
|
+
errorMessage?: string;
|
|
307
|
+
github: GitHubTaskContext;
|
|
308
|
+
}): Promise<string> {
|
|
309
|
+
const run: AIRun =
|
|
310
|
+
data.errorMessage === undefined
|
|
311
|
+
? aiRun({ status: data.status })
|
|
312
|
+
: aiRun({ status: data.status, errorMessage: data.errorMessage });
|
|
313
|
+
|
|
314
|
+
const posted: boolean = await GitHubRunReply.reportOutcome({
|
|
315
|
+
run: run,
|
|
316
|
+
github: data.github,
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
expect(posted).toBe(true);
|
|
320
|
+
|
|
321
|
+
return data.github.acknowledgementCommentId
|
|
322
|
+
? updatedComment().body
|
|
323
|
+
: createdComment().body;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/*
|
|
327
|
+
* One tick of ReportGitHubRunOutcomes, copied deliberately: report, and mark
|
|
328
|
+
* the run reported ONLY if GitHub accepted the write. Several tests below
|
|
329
|
+
* assert against this rather than against reportOutcome alone, because the
|
|
330
|
+
* bug worth catching lives in the pairing, not in either half.
|
|
331
|
+
*/
|
|
332
|
+
async function sweepOnce(data: {
|
|
333
|
+
run: AIRun;
|
|
334
|
+
github: GitHubTaskContext;
|
|
335
|
+
}): Promise<boolean> {
|
|
336
|
+
const posted: boolean = await GitHubRunReply.reportOutcome({
|
|
337
|
+
run: data.run,
|
|
338
|
+
github: data.github,
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
if (posted) {
|
|
342
|
+
await GitHubRunReply.markReported({
|
|
343
|
+
aiRunId: data.run.id!,
|
|
344
|
+
taskContext: data.run.taskContext,
|
|
345
|
+
github: data.github,
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
return posted;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// =========================================================================
|
|
353
|
+
|
|
354
|
+
describe("buildRunUrl / buildProjectRunsUrl", () => {
|
|
355
|
+
test("a run's page lives underneath its project's runs page", () => {
|
|
356
|
+
const runsUrl: string = GitHubRunReply.buildProjectRunsUrl(projectId);
|
|
357
|
+
const runUrl: string = GitHubRunReply.buildRunUrl({
|
|
358
|
+
projectId: projectId,
|
|
359
|
+
aiRunId: aiRunId,
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
expect(runUrl).toBe(`${runsUrl}/${aiRunId.toString()}`);
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
test("both URLs are rooted at the dashboard and scoped to the project", () => {
|
|
366
|
+
const dashboard: string = DashboardClientUrl.toString();
|
|
367
|
+
|
|
368
|
+
expect(
|
|
369
|
+
GitHubRunReply.buildProjectRunsUrl(projectId).startsWith(
|
|
370
|
+
`${dashboard}/${projectId.toString()}/`,
|
|
371
|
+
),
|
|
372
|
+
).toBe(true);
|
|
373
|
+
|
|
374
|
+
expect(
|
|
375
|
+
GitHubRunReply.buildRunUrl({
|
|
376
|
+
projectId: projectId,
|
|
377
|
+
aiRunId: aiRunId,
|
|
378
|
+
}).startsWith(`${dashboard}/${projectId.toString()}/`),
|
|
379
|
+
).toBe(true);
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
/*
|
|
383
|
+
* A template that lost an interpolation still returns a plausible-looking
|
|
384
|
+
* string — and a comment linking ".../undefined/ai/agents" is a dead link
|
|
385
|
+
* posted into someone else's repository. Asserted on the path only, so
|
|
386
|
+
* this does not depend on how the host is configured.
|
|
387
|
+
*/
|
|
388
|
+
test("neither URL can contain a lost interpolation", () => {
|
|
389
|
+
const dashboard: string = DashboardClientUrl.toString();
|
|
390
|
+
|
|
391
|
+
const runPath: string = GitHubRunReply.buildRunUrl({
|
|
392
|
+
projectId: projectId,
|
|
393
|
+
aiRunId: aiRunId,
|
|
394
|
+
}).substring(dashboard.length);
|
|
395
|
+
|
|
396
|
+
const runsPath: string = GitHubRunReply.buildProjectRunsUrl(
|
|
397
|
+
projectId,
|
|
398
|
+
).substring(dashboard.length);
|
|
399
|
+
|
|
400
|
+
expect(runPath).toBe(
|
|
401
|
+
`/${projectId.toString()}/ai/agents/${aiRunId.toString()}`,
|
|
402
|
+
);
|
|
403
|
+
expect(runsPath).toBe(`/${projectId.toString()}/ai/agents`);
|
|
404
|
+
expect(runPath).not.toContain("undefined");
|
|
405
|
+
expect(runPath).not.toContain("[object Object]");
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
test("two projects never resolve to the same run link", () => {
|
|
409
|
+
const otherProjectId: ObjectID = ObjectID.generate();
|
|
410
|
+
|
|
411
|
+
expect(
|
|
412
|
+
GitHubRunReply.buildRunUrl({
|
|
413
|
+
projectId: projectId,
|
|
414
|
+
aiRunId: aiRunId,
|
|
415
|
+
}),
|
|
416
|
+
).not.toBe(
|
|
417
|
+
GitHubRunReply.buildRunUrl({
|
|
418
|
+
projectId: otherProjectId,
|
|
419
|
+
aiRunId: aiRunId,
|
|
420
|
+
}),
|
|
421
|
+
);
|
|
422
|
+
});
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
// =========================================================================
|
|
426
|
+
|
|
427
|
+
describe("acknowledge", () => {
|
|
428
|
+
describe("the 👀 reaction", () => {
|
|
429
|
+
test("lands on the comment that asked, when a comment asked", async () => {
|
|
430
|
+
await GitHubRunReply.acknowledge({
|
|
431
|
+
aiRunId: aiRunId,
|
|
432
|
+
projectId: projectId,
|
|
433
|
+
projectName: "Acme",
|
|
434
|
+
github: issueContext(),
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
expect(addReactionToCommentSpy).toHaveBeenCalledTimes(1);
|
|
438
|
+
expect(addReactionToIssueSpy).not.toHaveBeenCalled();
|
|
439
|
+
expect(reactionOnComment().commentId).toBe(TRIGGER_COMMENT_ID);
|
|
440
|
+
expect(reactionOnComment().reaction).toBe(GitHubReaction.Eyes);
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
/*
|
|
444
|
+
* A trigger LABEL and an assignment carry no comment at all. Reacting to
|
|
445
|
+
* "the comment" there would either throw or, worse, react on comment id
|
|
446
|
+
* undefined — so the issue itself is the only thing left to mark, and
|
|
447
|
+
* without it those two triggers look completely ignored until the run
|
|
448
|
+
* finishes minutes later.
|
|
449
|
+
*/
|
|
450
|
+
test("lands on the issue itself when a label or an assignment triggered the run", async () => {
|
|
451
|
+
await GitHubRunReply.acknowledge({
|
|
452
|
+
aiRunId: aiRunId,
|
|
453
|
+
projectId: projectId,
|
|
454
|
+
projectName: "Acme",
|
|
455
|
+
github: issueContext({ triggerCommentId: undefined }),
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
expect(addReactionToIssueSpy).toHaveBeenCalledTimes(1);
|
|
459
|
+
expect(addReactionToCommentSpy).not.toHaveBeenCalled();
|
|
460
|
+
expect(reactionOnIssue().issueNumber).toBe(ISSUE_NUMBER);
|
|
461
|
+
expect(reactionOnIssue().reaction).toBe(GitHubReaction.Eyes);
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
// 0 is a real GitHub id in nobody's world, but it is falsy in everyone's.
|
|
465
|
+
test("treats a zero trigger comment id as no comment rather than reacting on id 0", async () => {
|
|
466
|
+
await GitHubRunReply.acknowledge({
|
|
467
|
+
aiRunId: aiRunId,
|
|
468
|
+
projectId: projectId,
|
|
469
|
+
projectName: "Acme",
|
|
470
|
+
github: issueContext({ triggerCommentId: 0 }),
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
expect(addReactionToCommentSpy).not.toHaveBeenCalled();
|
|
474
|
+
expect(addReactionToIssueSpy).toHaveBeenCalledTimes(1);
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
test("reacts in the repository the context names, as its installation", async () => {
|
|
478
|
+
await GitHubRunReply.acknowledge({
|
|
479
|
+
aiRunId: aiRunId,
|
|
480
|
+
projectId: projectId,
|
|
481
|
+
projectName: "Acme",
|
|
482
|
+
github: issueContext(),
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
expect(reactionOnComment().organizationName).toBe("acme");
|
|
486
|
+
expect(reactionOnComment().repositoryName).toBe("checkout");
|
|
487
|
+
expect(reactionOnComment().installationId).toBe("12345678");
|
|
488
|
+
});
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
describe("the acknowledgement comment", () => {
|
|
492
|
+
test("posts exactly one comment and returns its id for the outcome to edit", async () => {
|
|
493
|
+
const commentId: number | null = await GitHubRunReply.acknowledge({
|
|
494
|
+
aiRunId: aiRunId,
|
|
495
|
+
projectId: projectId,
|
|
496
|
+
projectName: "Acme",
|
|
497
|
+
github: issueContext(),
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
expect(createIssueCommentSpy).toHaveBeenCalledTimes(1);
|
|
501
|
+
expect(commentId).toBe(ACKNOWLEDGEMENT_COMMENT_ID);
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
test("posts into the issue the command came from", async () => {
|
|
505
|
+
await GitHubRunReply.acknowledge({
|
|
506
|
+
aiRunId: aiRunId,
|
|
507
|
+
projectId: projectId,
|
|
508
|
+
projectName: "Acme",
|
|
509
|
+
github: issueContext(),
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
expect(createdComment().issueNumber).toBe(ISSUE_NUMBER);
|
|
513
|
+
});
|
|
514
|
+
|
|
515
|
+
/*
|
|
516
|
+
* GitHub numbers issues and pull requests in one sequence and answers for
|
|
517
|
+
* both at /issues/{n}, so a pull request command must address the PULL
|
|
518
|
+
* REQUEST number. Falling back to the unset issueNumber would post to
|
|
519
|
+
* `/issues/undefined` — or, on a repository that happens to have an issue
|
|
520
|
+
* with that number, into a stranger's thread.
|
|
521
|
+
*/
|
|
522
|
+
test("posts into the pull request's own thread, not a same-numbered issue", async () => {
|
|
523
|
+
await GitHubRunReply.acknowledge({
|
|
524
|
+
aiRunId: aiRunId,
|
|
525
|
+
projectId: projectId,
|
|
526
|
+
projectName: "Acme",
|
|
527
|
+
github: pullRequestContext(),
|
|
528
|
+
});
|
|
529
|
+
|
|
530
|
+
expect(createdComment().issueNumber).toBe(PULL_REQUEST_NUMBER);
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
test("links the run and names the project the run is billed to", async () => {
|
|
534
|
+
await GitHubRunReply.acknowledge({
|
|
535
|
+
aiRunId: aiRunId,
|
|
536
|
+
projectId: projectId,
|
|
537
|
+
projectName: "Acme Platform",
|
|
538
|
+
github: issueContext(),
|
|
539
|
+
});
|
|
540
|
+
|
|
541
|
+
expect(createdComment().body).toContain(
|
|
542
|
+
GitHubRunReply.buildRunUrl({
|
|
543
|
+
projectId: projectId,
|
|
544
|
+
aiRunId: aiRunId,
|
|
545
|
+
}),
|
|
546
|
+
);
|
|
547
|
+
expect(createdComment().body).toContain("Acme Platform");
|
|
548
|
+
});
|
|
549
|
+
|
|
550
|
+
test("still posts a usable acknowledgement when the project has no name", async () => {
|
|
551
|
+
await GitHubRunReply.acknowledge({
|
|
552
|
+
aiRunId: aiRunId,
|
|
553
|
+
projectId: projectId,
|
|
554
|
+
projectName: undefined,
|
|
555
|
+
github: issueContext(),
|
|
556
|
+
});
|
|
557
|
+
|
|
558
|
+
expect(createdComment().body).toContain("On it");
|
|
559
|
+
expect(createdComment().body).not.toContain("undefined");
|
|
560
|
+
});
|
|
561
|
+
|
|
562
|
+
test("describes the command it is about to run", async () => {
|
|
563
|
+
await GitHubRunReply.acknowledge({
|
|
564
|
+
aiRunId: aiRunId,
|
|
565
|
+
projectId: projectId,
|
|
566
|
+
projectName: "Acme",
|
|
567
|
+
github: pullRequestContext({
|
|
568
|
+
commandType: GitHubCommandType.Review,
|
|
569
|
+
}),
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
expect(createdComment().body).toContain("reviewing this pull request");
|
|
573
|
+
});
|
|
574
|
+
|
|
575
|
+
/*
|
|
576
|
+
* The user's own words are echoed back so they can see what was heard.
|
|
577
|
+
* They are also arbitrary text from a stranger's comment, and a verbatim
|
|
578
|
+
* echo of "@oneuptime implement this" is a webhook this app sends to
|
|
579
|
+
* itself — a comment loop that bills the project until someone notices.
|
|
580
|
+
* Blockquoting is what stops it, so every line has to carry the marker.
|
|
581
|
+
*/
|
|
582
|
+
test("quotes an instruction that mentions the app so the echo cannot re-trigger it", async () => {
|
|
583
|
+
await GitHubRunReply.acknowledge({
|
|
584
|
+
aiRunId: aiRunId,
|
|
585
|
+
projectId: projectId,
|
|
586
|
+
projectName: "Acme",
|
|
587
|
+
github: issueContext({
|
|
588
|
+
instruction: "@oneuptime implement this\n@oneuptime and hurry",
|
|
589
|
+
}),
|
|
590
|
+
});
|
|
591
|
+
|
|
592
|
+
const body: string = createdComment().body;
|
|
593
|
+
|
|
594
|
+
expect(body).toContain("> @oneuptime implement this");
|
|
595
|
+
expect(body).toContain("> @oneuptime and hurry");
|
|
596
|
+
|
|
597
|
+
for (const line of body.split("\n")) {
|
|
598
|
+
if (line.includes("@oneuptime")) {
|
|
599
|
+
expect(line.startsWith(">")).toBe(true);
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
test("quotes a unicode instruction line by line without dropping it", async () => {
|
|
605
|
+
await GitHubRunReply.acknowledge({
|
|
606
|
+
aiRunId: aiRunId,
|
|
607
|
+
projectId: projectId,
|
|
608
|
+
projectName: "Acme",
|
|
609
|
+
github: issueContext({
|
|
610
|
+
instruction: "修复这个 bug\n🙏 пожалуйста",
|
|
611
|
+
}),
|
|
612
|
+
});
|
|
613
|
+
|
|
614
|
+
expect(createdComment().body).toContain("> 修复这个 bug");
|
|
615
|
+
expect(createdComment().body).toContain("> 🙏 пожалуйста");
|
|
616
|
+
});
|
|
617
|
+
|
|
618
|
+
test("posts a bare acknowledgement for an empty instruction rather than an empty quote", async () => {
|
|
619
|
+
await GitHubRunReply.acknowledge({
|
|
620
|
+
aiRunId: aiRunId,
|
|
621
|
+
projectId: projectId,
|
|
622
|
+
projectName: "Acme",
|
|
623
|
+
github: issueContext({ instruction: " \n " }),
|
|
624
|
+
});
|
|
625
|
+
|
|
626
|
+
expect(createdComment().body).toContain("On it");
|
|
627
|
+
expect(createdComment().body).not.toContain("You asked:");
|
|
628
|
+
});
|
|
629
|
+
});
|
|
630
|
+
|
|
631
|
+
/*
|
|
632
|
+
* The run is ALREADY QUEUED by the time this is called. Failing the command
|
|
633
|
+
* because a comment did not post would take a working fix away from the
|
|
634
|
+
* user to punish GitHub for a 502.
|
|
635
|
+
*/
|
|
636
|
+
describe("when GitHub refuses the acknowledgement", () => {
|
|
637
|
+
beforeEach(() => {
|
|
638
|
+
createIssueCommentSpy.mockRejectedValue(
|
|
639
|
+
new Error("503 Service Unavailable"),
|
|
640
|
+
);
|
|
641
|
+
});
|
|
642
|
+
|
|
643
|
+
test("returns null instead of throwing, so the queued run still runs", async () => {
|
|
644
|
+
await expect(
|
|
645
|
+
GitHubRunReply.acknowledge({
|
|
646
|
+
aiRunId: aiRunId,
|
|
647
|
+
projectId: projectId,
|
|
648
|
+
projectName: "Acme",
|
|
649
|
+
github: issueContext(),
|
|
650
|
+
}),
|
|
651
|
+
).resolves.toBeNull();
|
|
652
|
+
});
|
|
653
|
+
|
|
654
|
+
test("logs the run it could not acknowledge so an operator can find it", async () => {
|
|
655
|
+
await GitHubRunReply.acknowledge({
|
|
656
|
+
aiRunId: aiRunId,
|
|
657
|
+
projectId: projectId,
|
|
658
|
+
projectName: "Acme",
|
|
659
|
+
github: issueContext(),
|
|
660
|
+
});
|
|
661
|
+
|
|
662
|
+
expect(loggerErrorSpy).toHaveBeenCalledTimes(1);
|
|
663
|
+
expect(String(loggerErrorSpy.mock.calls[0]![0])).toContain(
|
|
664
|
+
aiRunId.toString(),
|
|
665
|
+
);
|
|
666
|
+
});
|
|
667
|
+
|
|
668
|
+
// Nothing about a failed comment says the run is not being worked on.
|
|
669
|
+
test("does not undo the reaction it already placed", async () => {
|
|
670
|
+
await GitHubRunReply.acknowledge({
|
|
671
|
+
aiRunId: aiRunId,
|
|
672
|
+
projectId: projectId,
|
|
673
|
+
projectName: "Acme",
|
|
674
|
+
github: issueContext(),
|
|
675
|
+
});
|
|
676
|
+
|
|
677
|
+
expect(addReactionToCommentSpy).toHaveBeenCalledTimes(1);
|
|
678
|
+
});
|
|
679
|
+
});
|
|
680
|
+
});
|
|
681
|
+
|
|
682
|
+
// =========================================================================
|
|
683
|
+
|
|
684
|
+
describe("reportOutcome", () => {
|
|
685
|
+
describe("one command produces one comment", () => {
|
|
686
|
+
test("EDITS the acknowledgement when there is one, and posts nothing new", async () => {
|
|
687
|
+
await GitHubRunReply.reportOutcome({
|
|
688
|
+
run: aiRun({ status: AIRunStatus.Completed }),
|
|
689
|
+
github: issueContext({
|
|
690
|
+
acknowledgementCommentId: ACKNOWLEDGEMENT_COMMENT_ID,
|
|
691
|
+
}),
|
|
692
|
+
});
|
|
693
|
+
|
|
694
|
+
expect(updateIssueCommentSpy).toHaveBeenCalledTimes(1);
|
|
695
|
+
expect(createIssueCommentSpy).not.toHaveBeenCalled();
|
|
696
|
+
expect(updatedComment().commentId).toBe(ACKNOWLEDGEMENT_COMMENT_ID);
|
|
697
|
+
});
|
|
698
|
+
|
|
699
|
+
/*
|
|
700
|
+
* The acknowledgement is best effort, so there may be nothing to edit.
|
|
701
|
+
* A run that then says nothing at all is the worst outcome available:
|
|
702
|
+
* the user sees no reply and no reaction and concludes nothing happened.
|
|
703
|
+
*/
|
|
704
|
+
test("posts a NEW comment when the acknowledgement never got posted", async () => {
|
|
705
|
+
await GitHubRunReply.reportOutcome({
|
|
706
|
+
run: aiRun({ status: AIRunStatus.Completed }),
|
|
707
|
+
github: issueContext({ acknowledgementCommentId: undefined }),
|
|
708
|
+
});
|
|
709
|
+
|
|
710
|
+
expect(createIssueCommentSpy).toHaveBeenCalledTimes(1);
|
|
711
|
+
expect(updateIssueCommentSpy).not.toHaveBeenCalled();
|
|
712
|
+
expect(createdComment().issueNumber).toBe(ISSUE_NUMBER);
|
|
713
|
+
});
|
|
714
|
+
|
|
715
|
+
test("the new comment goes to the pull request's own thread", async () => {
|
|
716
|
+
await GitHubRunReply.reportOutcome({
|
|
717
|
+
run: aiRun({ status: AIRunStatus.Completed }),
|
|
718
|
+
github: pullRequestContext({ acknowledgementCommentId: undefined }),
|
|
719
|
+
});
|
|
720
|
+
|
|
721
|
+
expect(createdComment().issueNumber).toBe(PULL_REQUEST_NUMBER);
|
|
722
|
+
});
|
|
723
|
+
|
|
724
|
+
// The whole acknowledge-then-report round trip, counted end to end.
|
|
725
|
+
test("acknowledging and then reporting leaves exactly one comment in the thread", async () => {
|
|
726
|
+
const github: GitHubTaskContext = issueContext();
|
|
727
|
+
|
|
728
|
+
const commentId: number | null = await GitHubRunReply.acknowledge({
|
|
729
|
+
aiRunId: aiRunId,
|
|
730
|
+
projectId: projectId,
|
|
731
|
+
projectName: "Acme",
|
|
732
|
+
github: github,
|
|
733
|
+
});
|
|
734
|
+
|
|
735
|
+
expect(commentId).not.toBeNull();
|
|
736
|
+
|
|
737
|
+
await GitHubRunReply.reportOutcome({
|
|
738
|
+
run: aiRun({ status: AIRunStatus.Completed }),
|
|
739
|
+
github: issueContext({ acknowledgementCommentId: commentId! }),
|
|
740
|
+
});
|
|
741
|
+
|
|
742
|
+
expect(createIssueCommentSpy).toHaveBeenCalledTimes(1);
|
|
743
|
+
expect(updateIssueCommentSpy).toHaveBeenCalledTimes(1);
|
|
744
|
+
});
|
|
745
|
+
});
|
|
746
|
+
|
|
747
|
+
describe("the return value is what makes the sweep retryable", () => {
|
|
748
|
+
test("returns true when GitHub accepts the edit", async () => {
|
|
749
|
+
await expect(
|
|
750
|
+
GitHubRunReply.reportOutcome({
|
|
751
|
+
run: aiRun({ status: AIRunStatus.Completed }),
|
|
752
|
+
github: issueContext({
|
|
753
|
+
acknowledgementCommentId: ACKNOWLEDGEMENT_COMMENT_ID,
|
|
754
|
+
}),
|
|
755
|
+
}),
|
|
756
|
+
).resolves.toBe(true);
|
|
757
|
+
});
|
|
758
|
+
|
|
759
|
+
/*
|
|
760
|
+
* A rejected EDIT is not a lost reply. The commonest cause is that
|
|
761
|
+
* somebody deleted the app's acknowledgement — an ordinary thing to do
|
|
762
|
+
* to a bot comment — and without the fallback the edit 404s forever:
|
|
763
|
+
* the outcome is never delivered and the sweep retries the same doomed
|
|
764
|
+
* write every minute until it ages out. A second comment beats silence.
|
|
765
|
+
*/
|
|
766
|
+
test("falls back to a NEW comment when the edit is rejected, and still succeeds", async () => {
|
|
767
|
+
updateIssueCommentSpy.mockRejectedValue(new Error("404 Not Found"));
|
|
768
|
+
|
|
769
|
+
await expect(
|
|
770
|
+
GitHubRunReply.reportOutcome({
|
|
771
|
+
run: aiRun({ status: AIRunStatus.Completed }),
|
|
772
|
+
github: issueContext({
|
|
773
|
+
acknowledgementCommentId: ACKNOWLEDGEMENT_COMMENT_ID,
|
|
774
|
+
}),
|
|
775
|
+
}),
|
|
776
|
+
).resolves.toBe(true);
|
|
777
|
+
|
|
778
|
+
expect(updateIssueCommentSpy).toHaveBeenCalledTimes(1);
|
|
779
|
+
expect(createIssueCommentSpy).toHaveBeenCalledTimes(1);
|
|
780
|
+
});
|
|
781
|
+
|
|
782
|
+
// Only when BOTH writes fail has the reply genuinely been lost.
|
|
783
|
+
test("returns false when the edit AND the fallback comment both fail", async () => {
|
|
784
|
+
updateIssueCommentSpy.mockRejectedValue(new Error("502 Bad Gateway"));
|
|
785
|
+
createIssueCommentSpy.mockRejectedValue(new Error("502 Bad Gateway"));
|
|
786
|
+
|
|
787
|
+
await expect(
|
|
788
|
+
GitHubRunReply.reportOutcome({
|
|
789
|
+
run: aiRun({ status: AIRunStatus.Completed }),
|
|
790
|
+
github: issueContext({
|
|
791
|
+
acknowledgementCommentId: ACKNOWLEDGEMENT_COMMENT_ID,
|
|
792
|
+
}),
|
|
793
|
+
}),
|
|
794
|
+
).resolves.toBe(false);
|
|
795
|
+
});
|
|
796
|
+
|
|
797
|
+
test("returns false when GitHub rejects the new comment", async () => {
|
|
798
|
+
createIssueCommentSpy.mockRejectedValue(new Error("502 Bad Gateway"));
|
|
799
|
+
|
|
800
|
+
await expect(
|
|
801
|
+
GitHubRunReply.reportOutcome({
|
|
802
|
+
run: aiRun({ status: AIRunStatus.Completed }),
|
|
803
|
+
github: issueContext({ acknowledgementCommentId: undefined }),
|
|
804
|
+
}),
|
|
805
|
+
).resolves.toBe(false);
|
|
806
|
+
});
|
|
807
|
+
|
|
808
|
+
test("names the run in the warning, so a lost reply is findable in the logs", async () => {
|
|
809
|
+
updateIssueCommentSpy.mockRejectedValue(new Error("502 Bad Gateway"));
|
|
810
|
+
createIssueCommentSpy.mockRejectedValue(new Error("502 Bad Gateway"));
|
|
811
|
+
|
|
812
|
+
await GitHubRunReply.reportOutcome({
|
|
813
|
+
run: aiRun({ status: AIRunStatus.Completed }),
|
|
814
|
+
github: issueContext({
|
|
815
|
+
acknowledgementCommentId: ACKNOWLEDGEMENT_COMMENT_ID,
|
|
816
|
+
}),
|
|
817
|
+
});
|
|
818
|
+
|
|
819
|
+
expect(loggerWarnSpy).toHaveBeenCalledTimes(1);
|
|
820
|
+
expect(String(loggerWarnSpy.mock.calls[0]![0])).toContain(
|
|
821
|
+
aiRunId.toString(),
|
|
822
|
+
);
|
|
823
|
+
});
|
|
824
|
+
|
|
825
|
+
/*
|
|
826
|
+
* The pairing, driven exactly as the sweeper drives it: a rejected write
|
|
827
|
+
* must leave the run UNMARKED. Marking it anyway would burn the user's
|
|
828
|
+
* only answer on a transient 502.
|
|
829
|
+
*/
|
|
830
|
+
test("a rejected write leaves the run unmarked, and the next sweep still edits the SAME comment", async () => {
|
|
831
|
+
const run: AIRun = aiRun({ status: AIRunStatus.Completed });
|
|
832
|
+
const github: GitHubTaskContext = issueContext({
|
|
833
|
+
acknowledgementCommentId: ACKNOWLEDGEMENT_COMMENT_ID,
|
|
834
|
+
});
|
|
835
|
+
|
|
836
|
+
/*
|
|
837
|
+
* Both writes fail on the first sweep — an outage, not a deleted
|
|
838
|
+
* comment — so there is genuinely nothing posted to mark.
|
|
839
|
+
*/
|
|
840
|
+
updateIssueCommentSpy.mockRejectedValueOnce(new Error("502"));
|
|
841
|
+
createIssueCommentSpy.mockRejectedValueOnce(new Error("502"));
|
|
842
|
+
|
|
843
|
+
await expect(sweepOnce({ run: run, github: github })).resolves.toBe(
|
|
844
|
+
false,
|
|
845
|
+
);
|
|
846
|
+
expect(updateOneByIdSpy).not.toHaveBeenCalled();
|
|
847
|
+
|
|
848
|
+
await expect(sweepOnce({ run: run, github: github })).resolves.toBe(
|
|
849
|
+
true,
|
|
850
|
+
);
|
|
851
|
+
expect(updateOneByIdSpy).toHaveBeenCalledTimes(1);
|
|
852
|
+
|
|
853
|
+
// Two attempts, one comment: the retry edited, it did not duplicate.
|
|
854
|
+
expect(updateIssueCommentSpy).toHaveBeenCalledTimes(2);
|
|
855
|
+
expect(createIssueCommentSpy).toHaveBeenCalledTimes(1);
|
|
856
|
+
});
|
|
857
|
+
|
|
858
|
+
test("a successful write is marked reported exactly once", async () => {
|
|
859
|
+
await sweepOnce({
|
|
860
|
+
run: aiRun({ status: AIRunStatus.Completed }),
|
|
861
|
+
github: issueContext({
|
|
862
|
+
acknowledgementCommentId: ACKNOWLEDGEMENT_COMMENT_ID,
|
|
863
|
+
}),
|
|
864
|
+
});
|
|
865
|
+
|
|
866
|
+
expect(updateOneByIdSpy).toHaveBeenCalledTimes(1);
|
|
867
|
+
});
|
|
868
|
+
|
|
869
|
+
/*
|
|
870
|
+
* A run without an id or a project cannot even build its own link. There
|
|
871
|
+
* is nothing useful to say, and saying it anyway would post a comment
|
|
872
|
+
* pointing at ".../undefined".
|
|
873
|
+
*/
|
|
874
|
+
/*
|
|
875
|
+
* Built WITHOUT an id rather than by clearing one: DatabaseBaseModel's
|
|
876
|
+
* `id` setter ignores a falsy value, so `run.id = null` leaves the id in
|
|
877
|
+
* place and the test would silently exercise the happy path instead.
|
|
878
|
+
*/
|
|
879
|
+
test("returns false and writes nothing for a run with no id", async () => {
|
|
880
|
+
const run: AIRun = new AIRun();
|
|
881
|
+
run.projectId = projectId;
|
|
882
|
+
run.status = AIRunStatus.Completed;
|
|
883
|
+
|
|
884
|
+
await expect(
|
|
885
|
+
GitHubRunReply.reportOutcome({
|
|
886
|
+
run: run,
|
|
887
|
+
github: issueContext({
|
|
888
|
+
acknowledgementCommentId: ACKNOWLEDGEMENT_COMMENT_ID,
|
|
889
|
+
}),
|
|
890
|
+
}),
|
|
891
|
+
).resolves.toBe(false);
|
|
892
|
+
|
|
893
|
+
expect(updateIssueCommentSpy).not.toHaveBeenCalled();
|
|
894
|
+
expect(createIssueCommentSpy).not.toHaveBeenCalled();
|
|
895
|
+
});
|
|
896
|
+
|
|
897
|
+
test("returns false and writes nothing for a run with no project", async () => {
|
|
898
|
+
const run: AIRun = new AIRun();
|
|
899
|
+
run.id = aiRunId;
|
|
900
|
+
run.status = AIRunStatus.Completed;
|
|
901
|
+
|
|
902
|
+
await expect(
|
|
903
|
+
GitHubRunReply.reportOutcome({
|
|
904
|
+
run: run,
|
|
905
|
+
github: issueContext({
|
|
906
|
+
acknowledgementCommentId: ACKNOWLEDGEMENT_COMMENT_ID,
|
|
907
|
+
}),
|
|
908
|
+
}),
|
|
909
|
+
).resolves.toBe(false);
|
|
910
|
+
|
|
911
|
+
expect(updateIssueCommentSpy).not.toHaveBeenCalled();
|
|
912
|
+
expect(createIssueCommentSpy).not.toHaveBeenCalled();
|
|
913
|
+
});
|
|
914
|
+
});
|
|
915
|
+
|
|
916
|
+
describe("what the comment says, per terminal status", () => {
|
|
917
|
+
test("Completed reads as done", async () => {
|
|
918
|
+
const body: string = await postedOutcomeBody({
|
|
919
|
+
status: AIRunStatus.Completed,
|
|
920
|
+
github: issueContext(),
|
|
921
|
+
});
|
|
922
|
+
|
|
923
|
+
expect(body).toContain("**Done**");
|
|
924
|
+
expect(body).toContain("working on this issue");
|
|
925
|
+
});
|
|
926
|
+
|
|
927
|
+
/*
|
|
928
|
+
* The distinction that matters most in this file. NoFixFound is a
|
|
929
|
+
* negative RESULT, not a failure: the agent read the code and had
|
|
930
|
+
* nothing worth proposing. A user who reads "I could not finish"
|
|
931
|
+
* retries the identical command; a user who reads "I looked and found
|
|
932
|
+
* nothing" writes a better instruction instead.
|
|
933
|
+
*/
|
|
934
|
+
test("NoFixFound reads as a finding and never as a failure", async () => {
|
|
935
|
+
const body: string = await postedOutcomeBody({
|
|
936
|
+
status: AIRunStatus.NoFixFound,
|
|
937
|
+
errorMessage: "The retry loop already backs off; nothing to change.",
|
|
938
|
+
github: issueContext(),
|
|
939
|
+
});
|
|
940
|
+
|
|
941
|
+
expect(body).toContain("I looked");
|
|
942
|
+
expect(body).toContain("do not have a change worth proposing");
|
|
943
|
+
expect(body).not.toContain("could not finish");
|
|
944
|
+
expect(body).not.toContain("❌");
|
|
945
|
+
});
|
|
946
|
+
|
|
947
|
+
test("NoFixFound uses errorMessage as the REASON it found nothing", async () => {
|
|
948
|
+
const body: string = await postedOutcomeBody({
|
|
949
|
+
status: AIRunStatus.NoFixFound,
|
|
950
|
+
errorMessage: "The retry loop already backs off; nothing to change.",
|
|
951
|
+
github: issueContext(),
|
|
952
|
+
});
|
|
953
|
+
|
|
954
|
+
expect(body).toContain(
|
|
955
|
+
"The retry loop already backs off; nothing to change.",
|
|
956
|
+
);
|
|
957
|
+
});
|
|
958
|
+
|
|
959
|
+
test("NoFixFound with no reason still says something true rather than blank", async () => {
|
|
960
|
+
const body: string = await postedOutcomeBody({
|
|
961
|
+
status: AIRunStatus.NoFixFound,
|
|
962
|
+
github: issueContext(),
|
|
963
|
+
});
|
|
964
|
+
|
|
965
|
+
expect(body).toContain("The agent finished without editing a file.");
|
|
966
|
+
expect(body).not.toContain("undefined");
|
|
967
|
+
});
|
|
968
|
+
|
|
969
|
+
test("NoFixFound invites a better instruction instead of a blind retry", async () => {
|
|
970
|
+
const body: string = await postedOutcomeBody({
|
|
971
|
+
status: AIRunStatus.NoFixFound,
|
|
972
|
+
github: issueContext(),
|
|
973
|
+
});
|
|
974
|
+
|
|
975
|
+
expect(body).toContain("Tell me more about what you want");
|
|
976
|
+
});
|
|
977
|
+
|
|
978
|
+
test("Error reads as a failure and carries the error message", async () => {
|
|
979
|
+
const body: string = await postedOutcomeBody({
|
|
980
|
+
status: AIRunStatus.Error,
|
|
981
|
+
errorMessage: "Could not clone acme/checkout: 403",
|
|
982
|
+
github: issueContext(),
|
|
983
|
+
});
|
|
984
|
+
|
|
985
|
+
expect(body).toContain("I could not finish this one.");
|
|
986
|
+
expect(body).toContain("Could not clone acme/checkout: 403");
|
|
987
|
+
});
|
|
988
|
+
|
|
989
|
+
test("Error with no message still points at the run log", async () => {
|
|
990
|
+
const body: string = await postedOutcomeBody({
|
|
991
|
+
status: AIRunStatus.Error,
|
|
992
|
+
github: issueContext(),
|
|
993
|
+
});
|
|
994
|
+
|
|
995
|
+
expect(body).toContain("I could not finish this one.");
|
|
996
|
+
expect(body).toContain("for the full log");
|
|
997
|
+
expect(body).not.toContain("undefined");
|
|
998
|
+
});
|
|
999
|
+
|
|
1000
|
+
test("Cancelled says so, and says that pushed work stays pushed", async () => {
|
|
1001
|
+
const body: string = await postedOutcomeBody({
|
|
1002
|
+
status: AIRunStatus.Cancelled,
|
|
1003
|
+
github: issueContext(),
|
|
1004
|
+
});
|
|
1005
|
+
|
|
1006
|
+
expect(body).toContain("**Cancelled**");
|
|
1007
|
+
expect(body).toContain("Anything already pushed stays pushed");
|
|
1008
|
+
});
|
|
1009
|
+
|
|
1010
|
+
/*
|
|
1011
|
+
* This comment closes out ONE run's own acknowledgement, so it must not
|
|
1012
|
+
* quote a thread-wide count: "@mention cancel" stopping three runs would
|
|
1013
|
+
* otherwise produce three comments each announcing "1 run on this
|
|
1014
|
+
* thread". The thread-wide count belongs to the reply that answers the
|
|
1015
|
+
* cancel command itself, which is a different composer method.
|
|
1016
|
+
*/
|
|
1017
|
+
test("Cancelled describes only itself, never a count of runs on the thread", async () => {
|
|
1018
|
+
const body: string = await postedOutcomeBody({
|
|
1019
|
+
status: AIRunStatus.Cancelled,
|
|
1020
|
+
github: issueContext(),
|
|
1021
|
+
});
|
|
1022
|
+
|
|
1023
|
+
expect(body).not.toContain("run on this thread");
|
|
1024
|
+
expect(body).not.toContain("runs on this thread");
|
|
1025
|
+
});
|
|
1026
|
+
|
|
1027
|
+
// ...and it links back, which is the one outcome a reader most wants.
|
|
1028
|
+
test("Cancelled links back to the run so the reader can see how far it got", async () => {
|
|
1029
|
+
const body: string = await postedOutcomeBody({
|
|
1030
|
+
status: AIRunStatus.Cancelled,
|
|
1031
|
+
github: issueContext(),
|
|
1032
|
+
});
|
|
1033
|
+
|
|
1034
|
+
expect(body).toContain(
|
|
1035
|
+
GitHubRunReply.buildRunUrl({
|
|
1036
|
+
projectId: projectId,
|
|
1037
|
+
aiRunId: aiRunId,
|
|
1038
|
+
}),
|
|
1039
|
+
);
|
|
1040
|
+
});
|
|
1041
|
+
|
|
1042
|
+
/*
|
|
1043
|
+
* A Stale run is one the heartbeat sweeper gave up on. From the reader's
|
|
1044
|
+
* side that is indistinguishable from any other failure, and it is
|
|
1045
|
+
* reported with the same words on purpose — what must NOT happen is a
|
|
1046
|
+
* stale run being reported as Done, or as having found nothing.
|
|
1047
|
+
*/
|
|
1048
|
+
test("Stale reads as a failure, not as a completion and not as a finding", async () => {
|
|
1049
|
+
const body: string = await postedOutcomeBody({
|
|
1050
|
+
status: AIRunStatus.Stale,
|
|
1051
|
+
errorMessage: "The agent stopped sending heartbeats.",
|
|
1052
|
+
github: issueContext(),
|
|
1053
|
+
});
|
|
1054
|
+
|
|
1055
|
+
expect(body).toContain("I could not finish this one.");
|
|
1056
|
+
expect(body).toContain("The agent stopped sending heartbeats.");
|
|
1057
|
+
expect(body).not.toContain("**Done**");
|
|
1058
|
+
expect(body).not.toContain("I looked");
|
|
1059
|
+
});
|
|
1060
|
+
|
|
1061
|
+
test("Completed, NoFixFound, Error and Cancelled are four distinguishable replies", async () => {
|
|
1062
|
+
const bodies: Array<string> = [];
|
|
1063
|
+
|
|
1064
|
+
for (const status of [
|
|
1065
|
+
AIRunStatus.Completed,
|
|
1066
|
+
AIRunStatus.NoFixFound,
|
|
1067
|
+
AIRunStatus.Error,
|
|
1068
|
+
AIRunStatus.Cancelled,
|
|
1069
|
+
]) {
|
|
1070
|
+
createIssueCommentSpy.mockClear();
|
|
1071
|
+
bodies.push(
|
|
1072
|
+
await postedOutcomeBody({
|
|
1073
|
+
status: status,
|
|
1074
|
+
errorMessage: "the same words for every status",
|
|
1075
|
+
github: issueContext(),
|
|
1076
|
+
}),
|
|
1077
|
+
);
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
expect(new Set<string>(bodies).size).toBe(4);
|
|
1081
|
+
});
|
|
1082
|
+
|
|
1083
|
+
test("every reply is signed, so a reader always knows what wrote it", async () => {
|
|
1084
|
+
const body: string = await postedOutcomeBody({
|
|
1085
|
+
status: AIRunStatus.Completed,
|
|
1086
|
+
github: issueContext(),
|
|
1087
|
+
});
|
|
1088
|
+
|
|
1089
|
+
expect(body).toContain("Posted by OneUptime");
|
|
1090
|
+
});
|
|
1091
|
+
|
|
1092
|
+
test("every reply links back to the run it is reporting", async () => {
|
|
1093
|
+
const body: string = await postedOutcomeBody({
|
|
1094
|
+
status: AIRunStatus.Error,
|
|
1095
|
+
github: issueContext(),
|
|
1096
|
+
});
|
|
1097
|
+
|
|
1098
|
+
expect(body).toContain(
|
|
1099
|
+
GitHubRunReply.buildRunUrl({
|
|
1100
|
+
projectId: projectId,
|
|
1101
|
+
aiRunId: aiRunId,
|
|
1102
|
+
}),
|
|
1103
|
+
);
|
|
1104
|
+
});
|
|
1105
|
+
});
|
|
1106
|
+
|
|
1107
|
+
describe("what the comment says the run produced", () => {
|
|
1108
|
+
/*
|
|
1109
|
+
* The revision pushed to the branch of the pull request this comment is
|
|
1110
|
+
* ON. Linking the reader to the page they are already reading is noise,
|
|
1111
|
+
* and a link to a DIFFERENT pull request would be a lie — a revision
|
|
1112
|
+
* opens nothing.
|
|
1113
|
+
*/
|
|
1114
|
+
test("a Revise run says it pushed to this pull request's branch", async () => {
|
|
1115
|
+
const body: string = await postedOutcomeBody({
|
|
1116
|
+
status: AIRunStatus.Completed,
|
|
1117
|
+
github: pullRequestContext({
|
|
1118
|
+
commandType: GitHubCommandType.Revise,
|
|
1119
|
+
}),
|
|
1120
|
+
});
|
|
1121
|
+
|
|
1122
|
+
expect(body).toContain("I pushed my changes to this pull request");
|
|
1123
|
+
expect(body).toContain("re-review the new commits");
|
|
1124
|
+
});
|
|
1125
|
+
|
|
1126
|
+
test("a Revise run lists NO pull request link and never asks the database for one", async () => {
|
|
1127
|
+
const body: string = await postedOutcomeBody({
|
|
1128
|
+
status: AIRunStatus.Completed,
|
|
1129
|
+
github: pullRequestContext({
|
|
1130
|
+
commandType: GitHubCommandType.Revise,
|
|
1131
|
+
}),
|
|
1132
|
+
});
|
|
1133
|
+
|
|
1134
|
+
expect(body).not.toContain("Opened");
|
|
1135
|
+
expect(body).not.toContain("github.com");
|
|
1136
|
+
expect(findPullRequestsSpy).not.toHaveBeenCalled();
|
|
1137
|
+
});
|
|
1138
|
+
|
|
1139
|
+
test("a Review run says the review is posted, and links nothing either", async () => {
|
|
1140
|
+
const body: string = await postedOutcomeBody({
|
|
1141
|
+
status: AIRunStatus.Completed,
|
|
1142
|
+
github: pullRequestContext({
|
|
1143
|
+
commandType: GitHubCommandType.Review,
|
|
1144
|
+
}),
|
|
1145
|
+
});
|
|
1146
|
+
|
|
1147
|
+
expect(body).toContain("My review is posted on this pull request.");
|
|
1148
|
+
expect(body).not.toContain("Opened");
|
|
1149
|
+
expect(findPullRequestsSpy).not.toHaveBeenCalled();
|
|
1150
|
+
});
|
|
1151
|
+
|
|
1152
|
+
test("an Implement run lists every recorded pull request by number and URL", async () => {
|
|
1153
|
+
findPullRequestsSpy.mockResolvedValue([
|
|
1154
|
+
pullRequestRow({
|
|
1155
|
+
pullRequestNumber: 12,
|
|
1156
|
+
url: "https://github.com/acme/checkout/pull/12",
|
|
1157
|
+
}),
|
|
1158
|
+
pullRequestRow({
|
|
1159
|
+
pullRequestNumber: 13,
|
|
1160
|
+
url: "https://github.com/acme/checkout/pull/13",
|
|
1161
|
+
}),
|
|
1162
|
+
]);
|
|
1163
|
+
|
|
1164
|
+
const body: string = await postedOutcomeBody({
|
|
1165
|
+
status: AIRunStatus.Completed,
|
|
1166
|
+
github: issueContext(),
|
|
1167
|
+
});
|
|
1168
|
+
|
|
1169
|
+
expect(body).toContain("Opened #12");
|
|
1170
|
+
expect(body).toContain("https://github.com/acme/checkout/pull/12");
|
|
1171
|
+
expect(body).toContain("Opened #13");
|
|
1172
|
+
expect(body).toContain("https://github.com/acme/checkout/pull/13");
|
|
1173
|
+
});
|
|
1174
|
+
|
|
1175
|
+
/*
|
|
1176
|
+
* "Done" with no artefact is not an answer — the user goes looking for a
|
|
1177
|
+
* pull request that does not exist and concludes the integration lies.
|
|
1178
|
+
* Saying so plainly is the only honest option left.
|
|
1179
|
+
*/
|
|
1180
|
+
test("an Implement run with NO recorded pull request says so instead of claiming one", async () => {
|
|
1181
|
+
findPullRequestsSpy.mockResolvedValue([]);
|
|
1182
|
+
|
|
1183
|
+
const body: string = await postedOutcomeBody({
|
|
1184
|
+
status: AIRunStatus.Completed,
|
|
1185
|
+
github: issueContext(),
|
|
1186
|
+
});
|
|
1187
|
+
|
|
1188
|
+
expect(body).toContain(
|
|
1189
|
+
"The run finished, but it did not record a pull request.",
|
|
1190
|
+
);
|
|
1191
|
+
expect(body).not.toContain("Opened #");
|
|
1192
|
+
});
|
|
1193
|
+
|
|
1194
|
+
test("a recorded pull request with no number is still announced", async () => {
|
|
1195
|
+
findPullRequestsSpy.mockResolvedValue([
|
|
1196
|
+
pullRequestRow({ url: "https://github.com/acme/checkout/pull/14" }),
|
|
1197
|
+
]);
|
|
1198
|
+
|
|
1199
|
+
const body: string = await postedOutcomeBody({
|
|
1200
|
+
status: AIRunStatus.Completed,
|
|
1201
|
+
github: issueContext(),
|
|
1202
|
+
});
|
|
1203
|
+
|
|
1204
|
+
expect(body).toContain(
|
|
1205
|
+
"Opened a pull request — https://github.com/acme/checkout/pull/14",
|
|
1206
|
+
);
|
|
1207
|
+
});
|
|
1208
|
+
|
|
1209
|
+
test("a recorded pull request with no URL prints an empty link, never the word undefined", async () => {
|
|
1210
|
+
findPullRequestsSpy.mockResolvedValue([
|
|
1211
|
+
pullRequestRow({ pullRequestNumber: 15 }),
|
|
1212
|
+
]);
|
|
1213
|
+
|
|
1214
|
+
const body: string = await postedOutcomeBody({
|
|
1215
|
+
status: AIRunStatus.Completed,
|
|
1216
|
+
github: issueContext(),
|
|
1217
|
+
});
|
|
1218
|
+
|
|
1219
|
+
expect(body).toContain("Opened #15");
|
|
1220
|
+
expect(body).not.toContain("undefined");
|
|
1221
|
+
});
|
|
1222
|
+
|
|
1223
|
+
test("the pull request lookup is scoped to this run and reads as root", async () => {
|
|
1224
|
+
findPullRequestsSpy.mockResolvedValue([]);
|
|
1225
|
+
|
|
1226
|
+
await postedOutcomeBody({
|
|
1227
|
+
status: AIRunStatus.Completed,
|
|
1228
|
+
github: issueContext(),
|
|
1229
|
+
});
|
|
1230
|
+
|
|
1231
|
+
const call: PullRequestFindByCall = nthCallArgument(
|
|
1232
|
+
findPullRequestsSpy,
|
|
1233
|
+
1,
|
|
1234
|
+
) as PullRequestFindByCall;
|
|
1235
|
+
|
|
1236
|
+
expect(call.query.aiRunId?.toString()).toBe(aiRunId.toString());
|
|
1237
|
+
expect(call.props.isRoot).toBe(true);
|
|
1238
|
+
expect(call.limit).toBe(LIMIT_MAX);
|
|
1239
|
+
expect(call.skip).toBe(0);
|
|
1240
|
+
});
|
|
1241
|
+
|
|
1242
|
+
// A failure has no artefacts to list; querying for them is dead weight.
|
|
1243
|
+
test("a failed Implement run never queries for pull requests", async () => {
|
|
1244
|
+
await postedOutcomeBody({
|
|
1245
|
+
status: AIRunStatus.Error,
|
|
1246
|
+
errorMessage: "boom",
|
|
1247
|
+
github: issueContext(),
|
|
1248
|
+
});
|
|
1249
|
+
|
|
1250
|
+
expect(findPullRequestsSpy).not.toHaveBeenCalled();
|
|
1251
|
+
});
|
|
1252
|
+
});
|
|
1253
|
+
|
|
1254
|
+
describe("the closing reaction", () => {
|
|
1255
|
+
test("🚀 on the trigger comment when the run completed", async () => {
|
|
1256
|
+
await GitHubRunReply.reportOutcome({
|
|
1257
|
+
run: aiRun({ status: AIRunStatus.Completed }),
|
|
1258
|
+
github: issueContext({
|
|
1259
|
+
acknowledgementCommentId: ACKNOWLEDGEMENT_COMMENT_ID,
|
|
1260
|
+
}),
|
|
1261
|
+
});
|
|
1262
|
+
|
|
1263
|
+
expect(reactionOnComment().reaction).toBe(GitHubReaction.Rocket);
|
|
1264
|
+
expect(reactionOnComment().commentId).toBe(TRIGGER_COMMENT_ID);
|
|
1265
|
+
});
|
|
1266
|
+
|
|
1267
|
+
test("😕 on the trigger comment for every non-completed status", async () => {
|
|
1268
|
+
for (const status of [
|
|
1269
|
+
AIRunStatus.NoFixFound,
|
|
1270
|
+
AIRunStatus.Error,
|
|
1271
|
+
AIRunStatus.Cancelled,
|
|
1272
|
+
AIRunStatus.Stale,
|
|
1273
|
+
]) {
|
|
1274
|
+
addReactionToCommentSpy.mockClear();
|
|
1275
|
+
|
|
1276
|
+
await GitHubRunReply.reportOutcome({
|
|
1277
|
+
run: aiRun({ status: status }),
|
|
1278
|
+
github: issueContext({
|
|
1279
|
+
acknowledgementCommentId: ACKNOWLEDGEMENT_COMMENT_ID,
|
|
1280
|
+
}),
|
|
1281
|
+
});
|
|
1282
|
+
|
|
1283
|
+
expect(reactionOnComment().reaction).toBe(GitHubReaction.Confused);
|
|
1284
|
+
}
|
|
1285
|
+
});
|
|
1286
|
+
|
|
1287
|
+
test("no reaction at all when a label or an assignment triggered the run", async () => {
|
|
1288
|
+
await GitHubRunReply.reportOutcome({
|
|
1289
|
+
run: aiRun({ status: AIRunStatus.Completed }),
|
|
1290
|
+
github: issueContext({
|
|
1291
|
+
triggerCommentId: undefined,
|
|
1292
|
+
acknowledgementCommentId: ACKNOWLEDGEMENT_COMMENT_ID,
|
|
1293
|
+
}),
|
|
1294
|
+
});
|
|
1295
|
+
|
|
1296
|
+
expect(addReactionToCommentSpy).not.toHaveBeenCalled();
|
|
1297
|
+
expect(addReactionToIssueSpy).not.toHaveBeenCalled();
|
|
1298
|
+
});
|
|
1299
|
+
|
|
1300
|
+
/*
|
|
1301
|
+
* The reaction is decoration; the comment is the answer. If a refused
|
|
1302
|
+
* reaction could drag the return value to false, the sweeper would post
|
|
1303
|
+
* the whole outcome again on the next tick — every minute, for a day.
|
|
1304
|
+
*/
|
|
1305
|
+
test("a refused reaction still reports true, so the reply is not posted twice", async () => {
|
|
1306
|
+
addReactionToCommentSpy.mockResolvedValue(false);
|
|
1307
|
+
|
|
1308
|
+
await expect(
|
|
1309
|
+
GitHubRunReply.reportOutcome({
|
|
1310
|
+
run: aiRun({ status: AIRunStatus.Completed }),
|
|
1311
|
+
github: issueContext({
|
|
1312
|
+
acknowledgementCommentId: ACKNOWLEDGEMENT_COMMENT_ID,
|
|
1313
|
+
}),
|
|
1314
|
+
}),
|
|
1315
|
+
).resolves.toBe(true);
|
|
1316
|
+
});
|
|
1317
|
+
|
|
1318
|
+
test("a refused reaction still lets the sweeper mark the run reported", async () => {
|
|
1319
|
+
addReactionToCommentSpy.mockResolvedValue(false);
|
|
1320
|
+
|
|
1321
|
+
await sweepOnce({
|
|
1322
|
+
run: aiRun({ status: AIRunStatus.Completed }),
|
|
1323
|
+
github: issueContext({
|
|
1324
|
+
acknowledgementCommentId: ACKNOWLEDGEMENT_COMMENT_ID,
|
|
1325
|
+
}),
|
|
1326
|
+
});
|
|
1327
|
+
|
|
1328
|
+
expect(updateOneByIdSpy).toHaveBeenCalledTimes(1);
|
|
1329
|
+
});
|
|
1330
|
+
|
|
1331
|
+
// The comment is written first; a reaction is never a precondition for it.
|
|
1332
|
+
test("the outcome comment is written before the reaction is attempted", async () => {
|
|
1333
|
+
addReactionToCommentSpy.mockResolvedValue(false);
|
|
1334
|
+
|
|
1335
|
+
await GitHubRunReply.reportOutcome({
|
|
1336
|
+
run: aiRun({ status: AIRunStatus.Completed }),
|
|
1337
|
+
github: issueContext({
|
|
1338
|
+
acknowledgementCommentId: ACKNOWLEDGEMENT_COMMENT_ID,
|
|
1339
|
+
}),
|
|
1340
|
+
});
|
|
1341
|
+
|
|
1342
|
+
expect(updateIssueCommentSpy.mock.invocationCallOrder[0]!).toBeLessThan(
|
|
1343
|
+
addReactionToCommentSpy.mock.invocationCallOrder[0]!,
|
|
1344
|
+
);
|
|
1345
|
+
});
|
|
1346
|
+
});
|
|
1347
|
+
});
|
|
1348
|
+
|
|
1349
|
+
// =========================================================================
|
|
1350
|
+
|
|
1351
|
+
describe("needsOutcomeReport", () => {
|
|
1352
|
+
/*
|
|
1353
|
+
* The sweeper does not just need "yes"; it needs the conversation back,
|
|
1354
|
+
* because it re-checks the installation binding against the project before
|
|
1355
|
+
* it writes anything (GHSA-xx95-gmcf-7q86) and then replies into the thread
|
|
1356
|
+
* this names.
|
|
1357
|
+
*/
|
|
1358
|
+
test("returns the conversation for a terminal run that has not reported yet", () => {
|
|
1359
|
+
const github: GitHubTaskContext = issueContext();
|
|
1360
|
+
|
|
1361
|
+
const returned: GitHubTaskContext | null =
|
|
1362
|
+
GitHubRunReply.needsOutcomeReport(
|
|
1363
|
+
aiRun({ status: AIRunStatus.Completed, github: github }),
|
|
1364
|
+
);
|
|
1365
|
+
|
|
1366
|
+
expect(returned).not.toBeNull();
|
|
1367
|
+
expect(returned!.installationId).toBe("12345678");
|
|
1368
|
+
expect(returned!.organizationName).toBe("acme");
|
|
1369
|
+
expect(returned!.repositoryName).toBe("checkout");
|
|
1370
|
+
expect(returned!.issueNumber).toBe(ISSUE_NUMBER);
|
|
1371
|
+
expect(returned!.commandType).toBe(GitHubCommandType.Implement);
|
|
1372
|
+
});
|
|
1373
|
+
|
|
1374
|
+
/*
|
|
1375
|
+
* Idempotency of the whole sweep, in one line. Without this the reply is
|
|
1376
|
+
* re-posted on every tick of a cron that runs every minute.
|
|
1377
|
+
*/
|
|
1378
|
+
test("returns null once the outcome has been reported", () => {
|
|
1379
|
+
expect(
|
|
1380
|
+
GitHubRunReply.needsOutcomeReport(
|
|
1381
|
+
aiRun({
|
|
1382
|
+
status: AIRunStatus.Completed,
|
|
1383
|
+
github: issueContext({
|
|
1384
|
+
reportedToGitHubAt: "2026-09-10T09:00:00.000Z",
|
|
1385
|
+
}),
|
|
1386
|
+
}),
|
|
1387
|
+
),
|
|
1388
|
+
).toBeNull();
|
|
1389
|
+
});
|
|
1390
|
+
|
|
1391
|
+
// Only a real timestamp counts as "already answered".
|
|
1392
|
+
test("an empty marker is not a report", () => {
|
|
1393
|
+
expect(
|
|
1394
|
+
GitHubRunReply.needsOutcomeReport(
|
|
1395
|
+
aiRun({
|
|
1396
|
+
status: AIRunStatus.Completed,
|
|
1397
|
+
github: issueContext({ reportedToGitHubAt: "" }),
|
|
1398
|
+
}),
|
|
1399
|
+
),
|
|
1400
|
+
).not.toBeNull();
|
|
1401
|
+
});
|
|
1402
|
+
|
|
1403
|
+
test("returns null for a run that did not come from GitHub", () => {
|
|
1404
|
+
expect(
|
|
1405
|
+
GitHubRunReply.needsOutcomeReport(
|
|
1406
|
+
aiRun({ status: AIRunStatus.Completed }),
|
|
1407
|
+
),
|
|
1408
|
+
).toBeNull();
|
|
1409
|
+
});
|
|
1410
|
+
|
|
1411
|
+
test("returns null for a run whose taskContext has no github key at all", () => {
|
|
1412
|
+
const run: AIRun = aiRun({ status: AIRunStatus.Completed });
|
|
1413
|
+
run.taskContext = { traceId: "abc" } as CodeFixTaskContext;
|
|
1414
|
+
|
|
1415
|
+
expect(GitHubRunReply.needsOutcomeReport(run)).toBeNull();
|
|
1416
|
+
});
|
|
1417
|
+
|
|
1418
|
+
/*
|
|
1419
|
+
* A half-built context cannot name where to reply. Answering anyway means
|
|
1420
|
+
* either a crash inside the sweep or a comment posted somewhere nobody
|
|
1421
|
+
* asked for one, so the only safe reading of malformed JSON is "no
|
|
1422
|
+
* conversation".
|
|
1423
|
+
*/
|
|
1424
|
+
test("returns null for a context missing the repository it belongs to", () => {
|
|
1425
|
+
const run: AIRun = aiRun({
|
|
1426
|
+
status: AIRunStatus.Completed,
|
|
1427
|
+
github: issueContext({ repositoryName: "" }),
|
|
1428
|
+
});
|
|
1429
|
+
|
|
1430
|
+
expect(GitHubRunReply.needsOutcomeReport(run)).toBeNull();
|
|
1431
|
+
});
|
|
1432
|
+
|
|
1433
|
+
test("returns null for a context missing its installation", () => {
|
|
1434
|
+
const run: AIRun = aiRun({
|
|
1435
|
+
status: AIRunStatus.Completed,
|
|
1436
|
+
github: issueContext({ installationId: " " }),
|
|
1437
|
+
});
|
|
1438
|
+
|
|
1439
|
+
expect(GitHubRunReply.needsOutcomeReport(run)).toBeNull();
|
|
1440
|
+
});
|
|
1441
|
+
|
|
1442
|
+
test("returns null for a context that names neither an issue nor a pull request", () => {
|
|
1443
|
+
const run: AIRun = aiRun({
|
|
1444
|
+
status: AIRunStatus.Completed,
|
|
1445
|
+
github: issueContext({
|
|
1446
|
+
issueNumber: undefined,
|
|
1447
|
+
pullRequestNumber: undefined,
|
|
1448
|
+
}),
|
|
1449
|
+
});
|
|
1450
|
+
|
|
1451
|
+
expect(GitHubRunReply.needsOutcomeReport(run)).toBeNull();
|
|
1452
|
+
});
|
|
1453
|
+
|
|
1454
|
+
// Both set is a bug, not a convenience: the two would disagree about where.
|
|
1455
|
+
test("returns null for a context that names BOTH an issue and a pull request", () => {
|
|
1456
|
+
const run: AIRun = aiRun({
|
|
1457
|
+
status: AIRunStatus.Completed,
|
|
1458
|
+
github: issueContext({
|
|
1459
|
+
issueNumber: ISSUE_NUMBER,
|
|
1460
|
+
pullRequestNumber: PULL_REQUEST_NUMBER,
|
|
1461
|
+
}),
|
|
1462
|
+
});
|
|
1463
|
+
|
|
1464
|
+
expect(GitHubRunReply.needsOutcomeReport(run)).toBeNull();
|
|
1465
|
+
});
|
|
1466
|
+
});
|
|
1467
|
+
|
|
1468
|
+
// =========================================================================
|
|
1469
|
+
|
|
1470
|
+
describe("markReported", () => {
|
|
1471
|
+
// Every field a context can carry, so "preserves the rest" means something.
|
|
1472
|
+
function fullContext(): GitHubTaskContext {
|
|
1473
|
+
return {
|
|
1474
|
+
codeRepositoryId: CODE_REPOSITORY_ID,
|
|
1475
|
+
installationId: "12345678",
|
|
1476
|
+
organizationName: "acme",
|
|
1477
|
+
repositoryName: "checkout",
|
|
1478
|
+
commandType: GitHubCommandType.Revise,
|
|
1479
|
+
instruction: "revise this — back off exponentially",
|
|
1480
|
+
pullRequestNumber: PULL_REQUEST_NUMBER,
|
|
1481
|
+
pullRequestHeadRefName: "oneuptime-ai/retry-backoff",
|
|
1482
|
+
isPullRequestFromFork: false,
|
|
1483
|
+
triggerCommentId: TRIGGER_COMMENT_ID,
|
|
1484
|
+
triggeredByLogin: "octocat",
|
|
1485
|
+
acknowledgementCommentId: ACKNOWLEDGEMENT_COMMENT_ID,
|
|
1486
|
+
webhookDeliveryId: "1a2b3c4d-0000-0000-0000-000000000000",
|
|
1487
|
+
};
|
|
1488
|
+
}
|
|
1489
|
+
|
|
1490
|
+
function writtenContext(): GitHubTaskContext {
|
|
1491
|
+
const call: UpdateOneByIdCall = nthCallArgument(
|
|
1492
|
+
updateOneByIdSpy,
|
|
1493
|
+
1,
|
|
1494
|
+
) as UpdateOneByIdCall;
|
|
1495
|
+
|
|
1496
|
+
const github: GitHubTaskContext | undefined =
|
|
1497
|
+
call.data.taskContext?.github;
|
|
1498
|
+
|
|
1499
|
+
if (!github) {
|
|
1500
|
+
throw new Error("markReported wrote no github context");
|
|
1501
|
+
}
|
|
1502
|
+
|
|
1503
|
+
return github;
|
|
1504
|
+
}
|
|
1505
|
+
|
|
1506
|
+
test("writes the marker onto the run it was given, as root", async () => {
|
|
1507
|
+
await GitHubRunReply.markReported({
|
|
1508
|
+
aiRunId: aiRunId,
|
|
1509
|
+
taskContext: { github: fullContext() },
|
|
1510
|
+
github: fullContext(),
|
|
1511
|
+
});
|
|
1512
|
+
|
|
1513
|
+
const call: UpdateOneByIdCall = nthCallArgument(
|
|
1514
|
+
updateOneByIdSpy,
|
|
1515
|
+
1,
|
|
1516
|
+
) as UpdateOneByIdCall;
|
|
1517
|
+
|
|
1518
|
+
expect(call.id.toString()).toBe(aiRunId.toString());
|
|
1519
|
+
expect(call.props.isRoot).toBe(true);
|
|
1520
|
+
});
|
|
1521
|
+
|
|
1522
|
+
test("writes an ISO 8601 timestamp that round-trips as a real date", async () => {
|
|
1523
|
+
await GitHubRunReply.markReported({
|
|
1524
|
+
aiRunId: aiRunId,
|
|
1525
|
+
taskContext: { github: fullContext() },
|
|
1526
|
+
github: fullContext(),
|
|
1527
|
+
});
|
|
1528
|
+
|
|
1529
|
+
const marker: string | undefined = writtenContext().reportedToGitHubAt;
|
|
1530
|
+
|
|
1531
|
+
expect(typeof marker).toBe("string");
|
|
1532
|
+
expect(new Date(marker!).toISOString()).toBe(marker);
|
|
1533
|
+
});
|
|
1534
|
+
|
|
1535
|
+
/*
|
|
1536
|
+
* The marker is written back into the run's own taskContext, so this is a
|
|
1537
|
+
* whole-object overwrite of the conversation. Dropping a field here would
|
|
1538
|
+
* lose the thread the run belongs to — and the acknowledgement comment id
|
|
1539
|
+
* in particular, which is the ONLY thing keeping a re-report from posting
|
|
1540
|
+
* a second comment.
|
|
1541
|
+
*/
|
|
1542
|
+
test("preserves every other field of the conversation", async () => {
|
|
1543
|
+
const github: GitHubTaskContext = fullContext();
|
|
1544
|
+
|
|
1545
|
+
await GitHubRunReply.markReported({
|
|
1546
|
+
aiRunId: aiRunId,
|
|
1547
|
+
taskContext: { github: github },
|
|
1548
|
+
github: github,
|
|
1549
|
+
});
|
|
1550
|
+
|
|
1551
|
+
const written: GitHubTaskContext = { ...writtenContext() };
|
|
1552
|
+
delete written.reportedToGitHubAt;
|
|
1553
|
+
|
|
1554
|
+
expect(written).toEqual(github);
|
|
1555
|
+
});
|
|
1556
|
+
|
|
1557
|
+
test("does not mutate the caller's context object", async () => {
|
|
1558
|
+
const github: GitHubTaskContext = fullContext();
|
|
1559
|
+
|
|
1560
|
+
await GitHubRunReply.markReported({
|
|
1561
|
+
aiRunId: aiRunId,
|
|
1562
|
+
taskContext: { github: github },
|
|
1563
|
+
github: github,
|
|
1564
|
+
});
|
|
1565
|
+
|
|
1566
|
+
expect(github.reportedToGitHubAt).toBeUndefined();
|
|
1567
|
+
});
|
|
1568
|
+
|
|
1569
|
+
test("replaces an existing marker rather than appending a second one", async () => {
|
|
1570
|
+
const github: GitHubTaskContext = {
|
|
1571
|
+
...fullContext(),
|
|
1572
|
+
reportedToGitHubAt: "2020-01-01T00:00:00.000Z",
|
|
1573
|
+
};
|
|
1574
|
+
|
|
1575
|
+
await GitHubRunReply.markReported({
|
|
1576
|
+
aiRunId: aiRunId,
|
|
1577
|
+
taskContext: { github: github },
|
|
1578
|
+
github: github,
|
|
1579
|
+
});
|
|
1580
|
+
|
|
1581
|
+
expect(writtenContext().reportedToGitHubAt).not.toBe(
|
|
1582
|
+
"2020-01-01T00:00:00.000Z",
|
|
1583
|
+
);
|
|
1584
|
+
});
|
|
1585
|
+
|
|
1586
|
+
// The round trip the sweeper depends on: what is written must read as done.
|
|
1587
|
+
test("a run carrying the written context no longer needs an outcome report", async () => {
|
|
1588
|
+
await GitHubRunReply.markReported({
|
|
1589
|
+
aiRunId: aiRunId,
|
|
1590
|
+
taskContext: { github: fullContext() },
|
|
1591
|
+
github: fullContext(),
|
|
1592
|
+
});
|
|
1593
|
+
|
|
1594
|
+
const run: AIRun = aiRun({
|
|
1595
|
+
status: AIRunStatus.Completed,
|
|
1596
|
+
github: writtenContext(),
|
|
1597
|
+
});
|
|
1598
|
+
|
|
1599
|
+
expect(GitHubRunReply.needsOutcomeReport(run)).toBeNull();
|
|
1600
|
+
});
|
|
1601
|
+
});
|
|
1602
|
+
|
|
1603
|
+
// =========================================================================
|
|
1604
|
+
|
|
1605
|
+
/*
|
|
1606
|
+
* "@mention status" reads these lines out. Someone looking at a pull request
|
|
1607
|
+
* wants "revising this pull request", not a run id and an enum name.
|
|
1608
|
+
*/
|
|
1609
|
+
describe("describeLiveRun", () => {
|
|
1610
|
+
function liveDescription(
|
|
1611
|
+
status: AIRunStatus,
|
|
1612
|
+
commandType: GitHubCommandType,
|
|
1613
|
+
): string {
|
|
1614
|
+
return GitHubRunReply.describeLiveRun(
|
|
1615
|
+
aiRun({
|
|
1616
|
+
status: status,
|
|
1617
|
+
github: pullRequestContext({ commandType: commandType }),
|
|
1618
|
+
}),
|
|
1619
|
+
);
|
|
1620
|
+
}
|
|
1621
|
+
|
|
1622
|
+
test("a queued run says it is waiting for an agent", () => {
|
|
1623
|
+
expect(
|
|
1624
|
+
liveDescription(AIRunStatus.Queued, GitHubCommandType.Review),
|
|
1625
|
+
).toBe("Reviewing this pull request — queued, waiting for an agent");
|
|
1626
|
+
});
|
|
1627
|
+
|
|
1628
|
+
test("a running run says it is in progress, not queued", () => {
|
|
1629
|
+
const text: string = liveDescription(
|
|
1630
|
+
AIRunStatus.Running,
|
|
1631
|
+
GitHubCommandType.Review,
|
|
1632
|
+
);
|
|
1633
|
+
|
|
1634
|
+
expect(text).toBe("Reviewing this pull request — in progress");
|
|
1635
|
+
expect(text).not.toContain("queued");
|
|
1636
|
+
});
|
|
1637
|
+
|
|
1638
|
+
test("a revision names the revision, queued and running alike", () => {
|
|
1639
|
+
expect(
|
|
1640
|
+
liveDescription(AIRunStatus.Queued, GitHubCommandType.Revise),
|
|
1641
|
+
).toContain("Revising this pull request");
|
|
1642
|
+
expect(
|
|
1643
|
+
liveDescription(AIRunStatus.Running, GitHubCommandType.Revise),
|
|
1644
|
+
).toContain("Revising this pull request");
|
|
1645
|
+
});
|
|
1646
|
+
|
|
1647
|
+
test("an implementation says it is working on the issue", () => {
|
|
1648
|
+
expect(
|
|
1649
|
+
GitHubRunReply.describeLiveRun(
|
|
1650
|
+
aiRun({
|
|
1651
|
+
status: AIRunStatus.Running,
|
|
1652
|
+
github: issueContext({ commandType: GitHubCommandType.Implement }),
|
|
1653
|
+
}),
|
|
1654
|
+
),
|
|
1655
|
+
).toBe("Working on this issue — in progress");
|
|
1656
|
+
});
|
|
1657
|
+
|
|
1658
|
+
/*
|
|
1659
|
+
* Only Queued gets the queue wording. A run paused for approval is not
|
|
1660
|
+
* waiting for an agent — telling the user it is would send them looking
|
|
1661
|
+
* for a capacity problem that does not exist.
|
|
1662
|
+
*/
|
|
1663
|
+
test("a run waiting for approval is in progress, not queued", () => {
|
|
1664
|
+
expect(
|
|
1665
|
+
liveDescription(
|
|
1666
|
+
AIRunStatus.WaitingForApproval,
|
|
1667
|
+
GitHubCommandType.Revise,
|
|
1668
|
+
),
|
|
1669
|
+
).toBe("Revising this pull request — in progress");
|
|
1670
|
+
});
|
|
1671
|
+
|
|
1672
|
+
test("a run with no GitHub conversation degrades to 'working' without throwing", () => {
|
|
1673
|
+
expect(
|
|
1674
|
+
GitHubRunReply.describeLiveRun(aiRun({ status: AIRunStatus.Running })),
|
|
1675
|
+
).toBe("working — in progress");
|
|
1676
|
+
});
|
|
1677
|
+
|
|
1678
|
+
test("a half-built conversation is not described as a specific command", () => {
|
|
1679
|
+
const run: AIRun = aiRun({
|
|
1680
|
+
status: AIRunStatus.Queued,
|
|
1681
|
+
github: pullRequestContext({ installationId: "" }),
|
|
1682
|
+
});
|
|
1683
|
+
|
|
1684
|
+
expect(GitHubRunReply.describeLiveRun(run)).toBe(
|
|
1685
|
+
"working — queued, waiting for an agent",
|
|
1686
|
+
);
|
|
1687
|
+
});
|
|
1688
|
+
|
|
1689
|
+
// Conversational commands never enqueue a run; the default must still be safe.
|
|
1690
|
+
test("an unexpected command type falls back to a plain description", () => {
|
|
1691
|
+
expect(
|
|
1692
|
+
GitHubRunReply.describeLiveRun(
|
|
1693
|
+
aiRun({
|
|
1694
|
+
status: AIRunStatus.Running,
|
|
1695
|
+
github: issueContext({ commandType: GitHubCommandType.Help }),
|
|
1696
|
+
}),
|
|
1697
|
+
),
|
|
1698
|
+
).toBe("Working — in progress");
|
|
1699
|
+
});
|
|
1700
|
+
|
|
1701
|
+
test("never leaks the run id into the thread", () => {
|
|
1702
|
+
expect(
|
|
1703
|
+
liveDescription(AIRunStatus.Running, GitHubCommandType.Review),
|
|
1704
|
+
).not.toContain(aiRunId.toString());
|
|
1705
|
+
});
|
|
1706
|
+
});
|
|
1707
|
+
});
|