@oneuptime/common 12.0.17 → 12.0.19
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/EnterpriseLicense.ts +23 -0
- package/Models/DatabaseModels/Index.ts +4 -2
- package/Models/DatabaseModels/Monitor.ts +39 -9
- package/Models/DatabaseModels/UserMicrosoftTeams.ts +344 -0
- package/Models/DatabaseModels/UserNotificationRule.ts +144 -8
- package/Models/DatabaseModels/UserNotificationSetting.ts +34 -0
- package/Models/DatabaseModels/UserOnCallLogTimeline.ts +95 -0
- package/Models/DatabaseModels/UserSlack.ts +341 -0
- package/Models/DatabaseModels/WorkspaceProjectAuthToken.ts +9 -0
- package/Server/API/UserMicrosoftTeamsAPI.ts +127 -0
- package/Server/API/UserSlackAPI.ts +125 -0
- package/Server/EnvironmentConfig.ts +12 -107
- package/Server/Infrastructure/Postgres/SchemaMigrations/1788700000000-RemoveMarketingConversionUploadState.ts +33 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1788800000000-DropMarketingConversionAddEnterpriseLicenseEmail.ts +51 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1788900000000-RedactStoredMonitorIngestSecrets.ts +117 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1789000000000-AddUserSlackAndMicrosoftTeams.ts +197 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +8 -0
- package/Server/Infrastructure/Queue.ts +7 -0
- package/Server/Services/BillingService.ts +8 -0
- package/Server/Services/EnterpriseLicenseService.ts +60 -0
- package/Server/Services/Index.ts +6 -2
- package/Server/Services/LlmLogService.ts +25 -9
- package/Server/Services/OnCallReadinessService.ts +86 -2
- package/Server/Services/ProjectService.ts +67 -0
- package/Server/Services/UserMicrosoftTeamsService.ts +208 -0
- package/Server/Services/UserNotificationMethodAdminService.ts +167 -5
- package/Server/Services/UserNotificationRuleAdminService.ts +40 -6
- package/Server/Services/UserNotificationRuleService.ts +589 -11
- package/Server/Services/UserNotificationSettingService.ts +138 -0
- package/Server/Services/UserService.ts +28 -0
- package/Server/Services/UserSlackService.ts +218 -0
- package/Server/Services/WorkspaceProjectAuthTokenService.ts +67 -1
- package/Server/Services/WorkspaceUserAuthTokenService.ts +73 -0
- package/Server/Services/WorkspaceUserNotificationService.ts +190 -0
- package/Server/Utils/Marketing/MarketingEventUtil.ts +145 -0
- package/Server/Utils/Marketing/MarketingEventWebhook.ts +114 -0
- package/Server/Utils/Monitor/MonitorLogUtil.ts +14 -1
- package/Server/Utils/Monitor/MonitorPayloadRedaction.ts +321 -0
- package/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.ts +191 -4
- package/Tests/App/Dashboard/AdminNotificationRulesPage.test.tsx +22 -14
- package/Tests/App/Dashboard/AdminUserNotificationMethodsPage.test.tsx +22 -11
- package/Tests/App/Dashboard/OnCallPreventionGuards.test.tsx +37 -1
- package/Tests/App/Dashboard/OnCallRulesTable.test.tsx +18 -10
- package/Tests/App/Dashboard/PayAsYouGoNotices.test.tsx +17 -0
- package/Tests/Models/DatabaseModels/DomainRoleTierCoverage.test.ts +27 -1
- package/Tests/Models/DatabaseModels/MonitorSecretKeyColumnAccessControl.test.ts +214 -0
- package/Tests/Server/API/OnCallReadinessAPI.test.ts +145 -26
- package/Tests/Server/API/UserNotificationMethodAdminAPI.test.ts +2 -2
- package/Tests/Server/Services/AIServiceDailyBudget.test.ts +11 -1
- package/Tests/Server/Services/AdminRuleEditGuards.test.ts +22 -8
- package/Tests/Server/Services/BillingService.test.ts +20 -0
- package/Tests/Server/Services/DeliverNotificationForRuleExtraction.test.ts +18 -2
- package/Tests/Server/Services/EpisodeRuleSeverityRepair.test.ts +5 -1
- package/Tests/Server/Services/LlmLogServiceTokenAggregateParams.test.ts +640 -0
- package/Tests/Server/Services/NotificationChannelEventCoverage.test.ts +246 -22
- package/Tests/Server/Services/NotificationDeletionImpact.test.ts +136 -8
- package/Tests/Server/Services/OnCallNotificationFallback.test.ts +191 -8
- package/Tests/Server/Services/OnCallReadinessService.test.ts +343 -18
- package/Tests/Server/Services/SeverityRuleBackfill.test.ts +5 -1
- package/Tests/Server/Services/UserMicrosoftTeamsService.test.ts +343 -0
- package/Tests/Server/Services/UserNotificationMethodAdminService.test.ts +209 -8
- package/Tests/Server/Services/UserNotificationRuleAdminGuards.test.ts +22 -6
- package/Tests/Server/Services/UserNotificationRuleDefaultCreation.test.ts +12 -6
- package/Tests/Server/Services/UserNotificationRuleExecuteItem.test.ts +37 -3
- package/Tests/Server/Services/UserNotificationRuleWorkspaceDelivery.test.ts +669 -0
- package/Tests/Server/Services/UserNotificationSettingWorkspaceChannels.test.ts +410 -0
- package/Tests/Server/Services/UserSlackService.test.ts +407 -0
- package/Tests/Server/Services/WorkspaceProjectAuthTokenDisconnectCascade.test.ts +224 -0
- package/Tests/Server/Services/WorkspaceUserAuthTokenNotificationMethodCascade.test.ts +189 -0
- package/Tests/Server/Services/WorkspaceUserNotificationService.test.ts +496 -0
- package/Tests/Server/Types/Database/Permissions/AdminNotificationRuleAccess.test.ts +30 -17
- package/Tests/Server/Types/Database/Permissions/CreateOwnershipScoping.test.ts +8 -3
- package/Tests/Server/Types/Database/Permissions/OwnerOnlyColumns.test.ts +72 -3
- package/Tests/Server/Types/Database/Permissions/UserNotificationRuleScoping.test.ts +22 -18
- package/Tests/Server/Types/Database/Permissions/WorkspaceMethodStampedColumnCreate.test.ts +178 -0
- package/Tests/Server/Utils/Marketing/MarketingEventUtil.test.ts +259 -0
- package/Tests/Server/Utils/Marketing/MarketingEventWebhook.test.ts +257 -0
- package/Tests/Server/Utils/Monitor/MonitorLogUtilRedaction.test.ts +300 -0
- package/Tests/Server/Utils/Monitor/MonitorPayloadRedaction.test.ts +381 -0
- package/Tests/Server/Utils/Runbook/RunbookExecutePermission.test.ts +228 -0
- package/Tests/Server/Utils/SessionReplay/SessionReplayErasureTombstone.test.ts +189 -0
- package/Tests/Server/Utils/SessionReplay/SessionReplayUsage.test.ts +205 -0
- package/Tests/Server/Utils/Telemetry/AppMetrics.test.ts +275 -0
- package/Tests/Server/Utils/Telemetry/TelemetryContext.test.ts +279 -0
- package/Tests/Server/Utils/Workspace/MicrosoftTeamsDirectMessage.test.ts +454 -0
- package/Tests/Types/Kubernetes/KubernetesObjectParsers.test.ts +794 -0
- package/Tests/Types/Kubernetes/KubernetesRightSizing.test.ts +53 -0
- package/Tests/Types/Measurement/MeasurementAggregationType.test.ts +100 -0
- package/Tests/Types/Monitor/MonitorStepConfigHelpers.test.ts +114 -0
- package/Tests/Types/Workspace/WorkspaceType.test.ts +53 -0
- package/Tests/UI/Utils/NotificationMethodUtil.test.ts +137 -9
- package/Tests/UI/Utils/PermissionGate.test.ts +105 -0
- package/Tests/Utils/Dashboard/Components/DashboardComponentDefaults.test.ts +257 -0
- package/Types/Billing/PayAsYouGoPricing.ts +1 -1
- package/Types/Marketing/MarketingEvent.ts +97 -0
- package/UI/Utils/NotificationMethodUtil.ts +41 -1
- package/UI/Utils/PermissionGate.ts +56 -0
- package/build/dist/Models/DatabaseModels/EnterpriseLicense.js +24 -0
- package/build/dist/Models/DatabaseModels/EnterpriseLicense.js.map +1 -1
- package/build/dist/Models/DatabaseModels/Index.js +4 -2
- package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
- package/build/dist/Models/DatabaseModels/Monitor.js +39 -9
- package/build/dist/Models/DatabaseModels/Monitor.js.map +1 -1
- package/build/dist/Models/DatabaseModels/UserMicrosoftTeams.js +363 -0
- package/build/dist/Models/DatabaseModels/UserMicrosoftTeams.js.map +1 -0
- package/build/dist/Models/DatabaseModels/UserNotificationRule.js +145 -8
- package/build/dist/Models/DatabaseModels/UserNotificationRule.js.map +1 -1
- package/build/dist/Models/DatabaseModels/UserNotificationSetting.js +38 -0
- package/build/dist/Models/DatabaseModels/UserNotificationSetting.js.map +1 -1
- package/build/dist/Models/DatabaseModels/UserOnCallLogTimeline.js +96 -0
- package/build/dist/Models/DatabaseModels/UserOnCallLogTimeline.js.map +1 -1
- package/build/dist/Models/DatabaseModels/UserSlack.js +361 -0
- package/build/dist/Models/DatabaseModels/UserSlack.js.map +1 -0
- package/build/dist/Models/DatabaseModels/WorkspaceProjectAuthToken.js.map +1 -1
- package/build/dist/Server/API/UserMicrosoftTeamsAPI.js +82 -0
- package/build/dist/Server/API/UserMicrosoftTeamsAPI.js.map +1 -0
- package/build/dist/Server/API/UserSlackAPI.js +81 -0
- package/build/dist/Server/API/UserSlackAPI.js.map +1 -0
- package/build/dist/Server/EnvironmentConfig.js +10 -70
- package/build/dist/Server/EnvironmentConfig.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1788700000000-RemoveMarketingConversionUploadState.js +26 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1788700000000-RemoveMarketingConversionUploadState.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1788800000000-DropMarketingConversionAddEnterpriseLicenseEmail.js +37 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1788800000000-DropMarketingConversionAddEnterpriseLicenseEmail.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1788900000000-RedactStoredMonitorIngestSecrets.js +106 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1788900000000-RedactStoredMonitorIngestSecrets.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789000000000-AddUserSlackAndMicrosoftTeams.js +78 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789000000000-AddUserSlackAndMicrosoftTeams.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +8 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
- package/build/dist/Server/Infrastructure/Queue.js +7 -0
- package/build/dist/Server/Infrastructure/Queue.js.map +1 -1
- package/build/dist/Server/Services/BillingService.js +6 -0
- package/build/dist/Server/Services/BillingService.js.map +1 -1
- package/build/dist/Server/Services/EnterpriseLicenseService.js +65 -0
- package/build/dist/Server/Services/EnterpriseLicenseService.js.map +1 -1
- package/build/dist/Server/Services/Index.js +6 -2
- package/build/dist/Server/Services/Index.js.map +1 -1
- package/build/dist/Server/Services/LlmLogService.js +25 -9
- package/build/dist/Server/Services/LlmLogService.js.map +1 -1
- package/build/dist/Server/Services/OnCallReadinessService.js +74 -2
- package/build/dist/Server/Services/OnCallReadinessService.js.map +1 -1
- package/build/dist/Server/Services/ProjectService.js +56 -0
- package/build/dist/Server/Services/ProjectService.js.map +1 -1
- package/build/dist/Server/Services/UserMicrosoftTeamsService.js +183 -0
- package/build/dist/Server/Services/UserMicrosoftTeamsService.js.map +1 -0
- package/build/dist/Server/Services/UserNotificationMethodAdminService.js +138 -2
- package/build/dist/Server/Services/UserNotificationMethodAdminService.js.map +1 -1
- package/build/dist/Server/Services/UserNotificationRuleAdminService.js +32 -4
- package/build/dist/Server/Services/UserNotificationRuleAdminService.js.map +1 -1
- package/build/dist/Server/Services/UserNotificationRuleService.js +499 -67
- package/build/dist/Server/Services/UserNotificationRuleService.js.map +1 -1
- package/build/dist/Server/Services/UserNotificationSettingService.js +118 -0
- package/build/dist/Server/Services/UserNotificationSettingService.js.map +1 -1
- package/build/dist/Server/Services/UserService.js +26 -1
- package/build/dist/Server/Services/UserService.js.map +1 -1
- package/build/dist/Server/Services/UserSlackService.js +195 -0
- package/build/dist/Server/Services/UserSlackService.js.map +1 -0
- package/build/dist/Server/Services/WorkspaceProjectAuthTokenService.js +64 -1
- package/build/dist/Server/Services/WorkspaceProjectAuthTokenService.js.map +1 -1
- package/build/dist/Server/Services/WorkspaceUserAuthTokenService.js +70 -0
- package/build/dist/Server/Services/WorkspaceUserAuthTokenService.js.map +1 -1
- package/build/dist/Server/Services/WorkspaceUserNotificationService.js +129 -0
- package/build/dist/Server/Services/WorkspaceUserNotificationService.js.map +1 -0
- package/build/dist/Server/Utils/Marketing/MarketingEventUtil.js +94 -0
- package/build/dist/Server/Utils/Marketing/MarketingEventUtil.js.map +1 -0
- package/build/dist/Server/Utils/Marketing/MarketingEventWebhook.js +93 -0
- package/build/dist/Server/Utils/Marketing/MarketingEventWebhook.js.map +1 -0
- package/build/dist/Server/Utils/Monitor/MonitorLogUtil.js +12 -1
- package/build/dist/Server/Utils/Monitor/MonitorLogUtil.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorPayloadRedaction.js +233 -0
- package/build/dist/Server/Utils/Monitor/MonitorPayloadRedaction.js.map +1 -0
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js +141 -4
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js.map +1 -1
- package/build/dist/Types/Billing/PayAsYouGoPricing.js +1 -1
- package/build/dist/Types/Billing/PayAsYouGoPricing.js.map +1 -1
- package/build/dist/Types/Marketing/MarketingEvent.js +43 -0
- package/build/dist/Types/Marketing/MarketingEvent.js.map +1 -0
- package/build/dist/UI/Utils/NotificationMethodUtil.js +33 -1
- package/build/dist/UI/Utils/NotificationMethodUtil.js.map +1 -1
- package/build/dist/UI/Utils/PermissionGate.js +32 -0
- package/build/dist/UI/Utils/PermissionGate.js.map +1 -1
- package/package.json +1 -1
- package/Models/DatabaseModels/MarketingConversion.ts +0 -410
- package/Server/Services/MarketingConversionService.ts +0 -10
- package/Server/Utils/Marketing/ConversionUploadProvider.ts +0 -215
- package/Server/Utils/Marketing/ConversionUploadProviders.ts +0 -22
- package/Server/Utils/Marketing/Providers/GoogleAds.ts +0 -333
- package/Server/Utils/Marketing/Providers/LinkedIn.ts +0 -185
- package/Server/Utils/Marketing/Providers/Meta.ts +0 -182
- package/Server/Utils/Marketing/Providers/MicrosoftAds.ts +0 -223
- package/Server/Utils/Marketing/Providers/Reddit.ts +0 -208
- package/Tests/Server/Utils/Marketing/AdUploadableConversionTypes.test.ts +0 -315
- package/Tests/Server/Utils/Marketing/ConversionUploadProvider.test.ts +0 -300
- package/Tests/Server/Utils/Marketing/GoogleAds.test.ts +0 -592
- package/Tests/Server/Utils/Marketing/LinkedIn.test.ts +0 -282
- package/Tests/Server/Utils/Marketing/Meta.test.ts +0 -304
- package/Tests/Server/Utils/Marketing/MicrosoftAds.test.ts +0 -263
- package/Tests/Server/Utils/Marketing/Reddit.test.ts +0 -259
- package/Types/Marketing/MarketingConversion.ts +0 -53
- package/build/dist/Models/DatabaseModels/MarketingConversion.js +0 -445
- package/build/dist/Models/DatabaseModels/MarketingConversion.js.map +0 -1
- package/build/dist/Server/Services/MarketingConversionService.js +0 -9
- package/build/dist/Server/Services/MarketingConversionService.js.map +0 -1
- package/build/dist/Server/Utils/Marketing/ConversionUploadProvider.js +0 -93
- package/build/dist/Server/Utils/Marketing/ConversionUploadProvider.js.map +0 -1
- package/build/dist/Server/Utils/Marketing/ConversionUploadProviders.js +0 -20
- package/build/dist/Server/Utils/Marketing/ConversionUploadProviders.js.map +0 -1
- package/build/dist/Server/Utils/Marketing/Providers/GoogleAds.js +0 -221
- package/build/dist/Server/Utils/Marketing/Providers/GoogleAds.js.map +0 -1
- package/build/dist/Server/Utils/Marketing/Providers/LinkedIn.js +0 -137
- package/build/dist/Server/Utils/Marketing/Providers/LinkedIn.js.map +0 -1
- package/build/dist/Server/Utils/Marketing/Providers/Meta.js +0 -133
- package/build/dist/Server/Utils/Marketing/Providers/Meta.js.map +0 -1
- package/build/dist/Server/Utils/Marketing/Providers/MicrosoftAds.js +0 -143
- package/build/dist/Server/Utils/Marketing/Providers/MicrosoftAds.js.map +0 -1
- package/build/dist/Server/Utils/Marketing/Providers/Reddit.js +0 -145
- package/build/dist/Server/Utils/Marketing/Providers/Reddit.js.map +0 -1
- package/build/dist/Types/Marketing/MarketingConversion.js +0 -52
- package/build/dist/Types/Marketing/MarketingConversion.js.map +0 -1
|
@@ -0,0 +1,640 @@
|
|
|
1
|
+
import AIService, {
|
|
2
|
+
AUTONOMOUS_AI_FEATURES,
|
|
3
|
+
AI_ALERT_INVESTIGATION_FEATURE,
|
|
4
|
+
AI_INCIDENT_INVESTIGATION_FEATURE,
|
|
5
|
+
AutonomousBudgetStatus,
|
|
6
|
+
} from "../../../Server/Services/AIService";
|
|
7
|
+
import LlmLogService from "../../../Server/Services/LlmLogService";
|
|
8
|
+
import ProjectService from "../../../Server/Services/ProjectService";
|
|
9
|
+
import Project from "../../../Models/DatabaseModels/Project";
|
|
10
|
+
import ObjectID from "../../../Types/ObjectID";
|
|
11
|
+
import { describe, expect, test, afterEach } from "@jest/globals";
|
|
12
|
+
|
|
13
|
+
/*
|
|
14
|
+
* LlmLogService.getTotalTokensUsedSince builds its WHERE clause per subject
|
|
15
|
+
* lane, so the SQL text is not constant — but the bound parameter array has to
|
|
16
|
+
* agree with whichever branch was taken. It did not.
|
|
17
|
+
*
|
|
18
|
+
* PostgreSQL derives a prepared statement's parameter count from the HIGHEST
|
|
19
|
+
* $n in the statement text, never from what the driver sends. The incident
|
|
20
|
+
* branch referenced only $4 while the caller always bound five values, so
|
|
21
|
+
* every capped incident investigation died at Bind with
|
|
22
|
+
*
|
|
23
|
+
* bind message supplies 5 parameters, but prepared statement "" requires 4
|
|
24
|
+
*
|
|
25
|
+
* and the alert branch referenced $5 while never referencing $4, so it died at
|
|
26
|
+
* Parse with
|
|
27
|
+
*
|
|
28
|
+
* could not determine data type of parameter $4
|
|
29
|
+
*
|
|
30
|
+
* InvestigationQueue.enqueue() catches any budget-check failure and quietly
|
|
31
|
+
* skips the run, so both lanes lost automatic AI investigations with no
|
|
32
|
+
* user-visible signal beyond an empty "AI Logs" tab. Only projects that had
|
|
33
|
+
* configured a positive daily token ceiling were affected: a null limit
|
|
34
|
+
* returns before the query and an explicit 0 is the intended kill switch.
|
|
35
|
+
*
|
|
36
|
+
* These tests pin the placeholder/binding contract for every branch so the
|
|
37
|
+
* arrays and the clause that reads them cannot drift apart again.
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
/*
|
|
41
|
+
* Both messages below were reproduced against PostgreSQL 15.18 through the
|
|
42
|
+
* same `pg` driver TypeORM's manager.query() uses, with the exact SQL these
|
|
43
|
+
* branches generate. assertBindable() replays those two server-side rules in
|
|
44
|
+
* process so the suite needs no database.
|
|
45
|
+
*/
|
|
46
|
+
function assertBindable(sql: string, params: Array<unknown>): void {
|
|
47
|
+
const referenced: Set<number> = new Set<number>();
|
|
48
|
+
let highest: number = 0;
|
|
49
|
+
|
|
50
|
+
for (const match of sql.matchAll(/\$(\d+)/g)) {
|
|
51
|
+
const index: number = Number(match[1]);
|
|
52
|
+
referenced.add(index);
|
|
53
|
+
highest = Math.max(highest, index);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Parse: a hole below the highest placeholder has no inferable type.
|
|
57
|
+
for (let index: number = 1; index <= highest; index++) {
|
|
58
|
+
if (!referenced.has(index)) {
|
|
59
|
+
throw new Error(`could not determine data type of parameter $${index}`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Bind: the supplied count must equal the count the statement requires.
|
|
64
|
+
if (params.length !== highest) {
|
|
65
|
+
throw new Error(
|
|
66
|
+
`bind message supplies ${params.length} parameters, but prepared statement "" requires ${highest}`,
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
interface CapturedQuery {
|
|
72
|
+
sql: string;
|
|
73
|
+
params: Array<unknown>;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/*
|
|
77
|
+
* Every aggregate issued through this harness is checked against the bind
|
|
78
|
+
* contract as it is issued, so each test in this file enforces it even when
|
|
79
|
+
* the test itself is asserting something else.
|
|
80
|
+
*/
|
|
81
|
+
function captureQueries(
|
|
82
|
+
rows: Array<{ total: string | number | null }> = [{ total: "0" }],
|
|
83
|
+
): Array<CapturedQuery> {
|
|
84
|
+
const calls: Array<CapturedQuery> = [];
|
|
85
|
+
|
|
86
|
+
jest.spyOn(LlmLogService, "getRepository").mockReturnValue({
|
|
87
|
+
manager: {
|
|
88
|
+
query: (sql: string, params: Array<unknown>) => {
|
|
89
|
+
calls.push({ sql, params });
|
|
90
|
+
assertBindable(sql, params);
|
|
91
|
+
return Promise.resolve(rows);
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
} as any);
|
|
95
|
+
|
|
96
|
+
return calls;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const SINCE: Date = new Date("2026-08-07T00:00:00.000Z");
|
|
100
|
+
|
|
101
|
+
const INCIDENT_LEGACY: Array<string> = [
|
|
102
|
+
AI_INCIDENT_INVESTIGATION_FEATURE,
|
|
103
|
+
"AI Investigation Grading",
|
|
104
|
+
];
|
|
105
|
+
const ALERT_LEGACY: Array<string> = [AI_ALERT_INVESTIGATION_FEATURE];
|
|
106
|
+
|
|
107
|
+
interface Lane {
|
|
108
|
+
name: string;
|
|
109
|
+
subject: {
|
|
110
|
+
incidentId?: ObjectID | undefined;
|
|
111
|
+
alertId?: ObjectID | undefined;
|
|
112
|
+
};
|
|
113
|
+
expectedParamCount: number;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function lanes(): Array<Lane> {
|
|
117
|
+
return [
|
|
118
|
+
{
|
|
119
|
+
name: "incident",
|
|
120
|
+
subject: { incidentId: ObjectID.generate() },
|
|
121
|
+
expectedParamCount: 4,
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
name: "alert",
|
|
125
|
+
subject: { alertId: ObjectID.generate() },
|
|
126
|
+
expectedParamCount: 4,
|
|
127
|
+
},
|
|
128
|
+
{ name: "subjectless", subject: {}, expectedParamCount: 5 },
|
|
129
|
+
];
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async function aggregate(data: {
|
|
133
|
+
subject: {
|
|
134
|
+
incidentId?: ObjectID | undefined;
|
|
135
|
+
alertId?: ObjectID | undefined;
|
|
136
|
+
};
|
|
137
|
+
projectId?: ObjectID | undefined;
|
|
138
|
+
features?: Array<string> | undefined;
|
|
139
|
+
legacyIncidentFeatures?: Array<string> | undefined;
|
|
140
|
+
legacyAlertFeatures?: Array<string> | undefined;
|
|
141
|
+
rows?: Array<{ total: string | number | null }> | undefined;
|
|
142
|
+
}): Promise<{ calls: Array<CapturedQuery>; total: number }> {
|
|
143
|
+
const calls: Array<CapturedQuery> = captureQueries(data.rows);
|
|
144
|
+
|
|
145
|
+
const total: number = await LlmLogService.getTotalTokensUsedSince({
|
|
146
|
+
projectId: data.projectId || ObjectID.generate(),
|
|
147
|
+
since: SINCE,
|
|
148
|
+
features: data.features || AUTONOMOUS_AI_FEATURES,
|
|
149
|
+
legacyIncidentFeatures:
|
|
150
|
+
data.legacyIncidentFeatures === undefined
|
|
151
|
+
? INCIDENT_LEGACY
|
|
152
|
+
: data.legacyIncidentFeatures,
|
|
153
|
+
legacyAlertFeatures:
|
|
154
|
+
data.legacyAlertFeatures === undefined
|
|
155
|
+
? ALERT_LEGACY
|
|
156
|
+
: data.legacyAlertFeatures,
|
|
157
|
+
...data.subject,
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
return { calls, total };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function highestPlaceholder(sql: string): number {
|
|
164
|
+
return Math.max(
|
|
165
|
+
0,
|
|
166
|
+
...[...sql.matchAll(/\$(\d+)/g)].map((m: RegExpMatchArray) => {
|
|
167
|
+
return Number(m[1]);
|
|
168
|
+
}),
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
afterEach(() => {
|
|
173
|
+
jest.restoreAllMocks();
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
/*
|
|
177
|
+
* The emulator is only worth anything if it actually rejects the shipped bug.
|
|
178
|
+
* These four cases feed it the literal pre-fix clauses so a future edit that
|
|
179
|
+
* weakens assertBindable() cannot make the rest of this file pass vacuously.
|
|
180
|
+
*/
|
|
181
|
+
describe("PostgreSQL bind contract emulator", () => {
|
|
182
|
+
const PREFIX: string =
|
|
183
|
+
'SELECT COALESCE(SUM("log"."totalTokens"), 0) AS "total" FROM "LlmLog" AS "log" WHERE "log"."projectId" = $1 AND "log"."createdAt" >= $2 AND "log"."feature" = ANY($3)';
|
|
184
|
+
const FIVE: Array<unknown> = ["p", SINCE, ["a"], ["b"], ["c"]];
|
|
185
|
+
|
|
186
|
+
test("rejects the pre-fix incident clause with the reported bind error", () => {
|
|
187
|
+
expect(() => {
|
|
188
|
+
return assertBindable(
|
|
189
|
+
`${PREFIX} AND (("log"."feature" = ANY($4)))`,
|
|
190
|
+
FIVE,
|
|
191
|
+
);
|
|
192
|
+
}).toThrow(
|
|
193
|
+
'bind message supplies 5 parameters, but prepared statement "" requires 4',
|
|
194
|
+
);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
test("rejects the pre-fix alert clause, which skipped $4 entirely", () => {
|
|
198
|
+
expect(() => {
|
|
199
|
+
return assertBindable(
|
|
200
|
+
`${PREFIX} AND (("log"."feature" = ANY($5)))`,
|
|
201
|
+
FIVE,
|
|
202
|
+
);
|
|
203
|
+
}).toThrow("could not determine data type of parameter $4");
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
test("accepts the pre-fix subjectless clause, which was always consistent", () => {
|
|
207
|
+
expect(() => {
|
|
208
|
+
return assertBindable(
|
|
209
|
+
`${PREFIX} AND NOT ("log"."feature" = ANY($4)) AND NOT ("log"."feature" = ANY($5))`,
|
|
210
|
+
FIVE,
|
|
211
|
+
);
|
|
212
|
+
}).not.toThrow();
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
test("rejects binding fewer parameters than the text references", () => {
|
|
216
|
+
expect(() => {
|
|
217
|
+
return assertBindable(
|
|
218
|
+
`${PREFIX} AND NOT ("log"."feature" = ANY($4)) AND NOT ("log"."feature" = ANY($5))`,
|
|
219
|
+
FIVE.slice(0, 4),
|
|
220
|
+
);
|
|
221
|
+
}).toThrow(
|
|
222
|
+
'bind message supplies 4 parameters, but prepared statement "" requires 5',
|
|
223
|
+
);
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
describe("getTotalTokensUsedSince placeholder and binding contract", () => {
|
|
228
|
+
test.each(lanes())(
|
|
229
|
+
"$name lane binds exactly the parameters its SQL references",
|
|
230
|
+
async (lane: Lane) => {
|
|
231
|
+
const { calls } = await aggregate({ subject: lane.subject });
|
|
232
|
+
|
|
233
|
+
expect(calls).toHaveLength(1);
|
|
234
|
+
|
|
235
|
+
const call: CapturedQuery = calls[0]!;
|
|
236
|
+
expect(call.params).toHaveLength(lane.expectedParamCount);
|
|
237
|
+
expect(highestPlaceholder(call.sql)).toBe(lane.expectedParamCount);
|
|
238
|
+
},
|
|
239
|
+
);
|
|
240
|
+
|
|
241
|
+
test.each(lanes())(
|
|
242
|
+
"$name lane leaves no unreferenced placeholder below the highest",
|
|
243
|
+
async (lane: Lane) => {
|
|
244
|
+
const { calls } = await aggregate({ subject: lane.subject });
|
|
245
|
+
const sql: string = calls[0]!.sql;
|
|
246
|
+
|
|
247
|
+
for (let index: number = 1; index <= lane.expectedParamCount; index++) {
|
|
248
|
+
expect(sql).toContain(`$${index}`);
|
|
249
|
+
}
|
|
250
|
+
},
|
|
251
|
+
);
|
|
252
|
+
|
|
253
|
+
test.each(lanes())(
|
|
254
|
+
"$name lane binds project, since and features as $1..$3",
|
|
255
|
+
async (lane: Lane) => {
|
|
256
|
+
const projectId: ObjectID = ObjectID.generate();
|
|
257
|
+
const { calls } = await aggregate({ subject: lane.subject, projectId });
|
|
258
|
+
const params: Array<unknown> = calls[0]!.params;
|
|
259
|
+
|
|
260
|
+
expect(params[0]).toBe(projectId.toString());
|
|
261
|
+
expect(params[1]).toBe(SINCE);
|
|
262
|
+
expect(params[2]).toEqual(AUTONOMOUS_AI_FEATURES);
|
|
263
|
+
},
|
|
264
|
+
);
|
|
265
|
+
|
|
266
|
+
test("incident lane binds only its own legacy features, as $4", async () => {
|
|
267
|
+
const { calls } = await aggregate({
|
|
268
|
+
subject: { incidentId: ObjectID.generate() },
|
|
269
|
+
});
|
|
270
|
+
const call: CapturedQuery = calls[0]!;
|
|
271
|
+
|
|
272
|
+
expect(call.params).toHaveLength(4);
|
|
273
|
+
expect(call.params[3]).toEqual(INCIDENT_LEGACY);
|
|
274
|
+
expect(call.sql).toContain('"log"."feature" = ANY($4)');
|
|
275
|
+
expect(call.sql).not.toContain("$5");
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
test("alert lane binds only its own legacy features, as $4", async () => {
|
|
279
|
+
const { calls } = await aggregate({
|
|
280
|
+
subject: { alertId: ObjectID.generate() },
|
|
281
|
+
});
|
|
282
|
+
const call: CapturedQuery = calls[0]!;
|
|
283
|
+
|
|
284
|
+
expect(call.params).toHaveLength(4);
|
|
285
|
+
expect(call.params[3]).toEqual(ALERT_LEGACY);
|
|
286
|
+
expect(call.sql).toContain('"log"."feature" = ANY($4)');
|
|
287
|
+
expect(call.sql).not.toContain("$5");
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
test("subjectless lane binds both legacy lists, as $4 and $5", async () => {
|
|
291
|
+
const { calls } = await aggregate({ subject: {} });
|
|
292
|
+
const call: CapturedQuery = calls[0]!;
|
|
293
|
+
|
|
294
|
+
expect(call.params).toHaveLength(5);
|
|
295
|
+
expect(call.params[3]).toEqual(INCIDENT_LEGACY);
|
|
296
|
+
expect(call.params[4]).toEqual(ALERT_LEGACY);
|
|
297
|
+
expect(call.sql).toContain('NOT ("log"."feature" = ANY($4))');
|
|
298
|
+
expect(call.sql).toContain('NOT ("log"."feature" = ANY($5))');
|
|
299
|
+
});
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
/*
|
|
303
|
+
* The legacy lists are caller-supplied and vary by deployment age. The
|
|
304
|
+
* contract has to hold for every shape, not just the one AIService passes
|
|
305
|
+
* today — an empty list is what a fresh install sends.
|
|
306
|
+
*/
|
|
307
|
+
describe("binding contract holds across legacy feature list shapes", () => {
|
|
308
|
+
const shapes: Array<{ name: string; value: Array<string> }> = [
|
|
309
|
+
{ name: "empty", value: [] },
|
|
310
|
+
{ name: "single", value: ["AI Incident Investigation"] },
|
|
311
|
+
{ name: "many", value: ["a", "b", "c", "d", "e"] },
|
|
312
|
+
];
|
|
313
|
+
|
|
314
|
+
for (const lane of lanes()) {
|
|
315
|
+
for (const incident of shapes) {
|
|
316
|
+
for (const alert of shapes) {
|
|
317
|
+
test(`${lane.name} lane stays bindable with ${incident.name} incident and ${alert.name} alert legacy lists`, async () => {
|
|
318
|
+
const { calls } = await aggregate({
|
|
319
|
+
subject: lane.subject,
|
|
320
|
+
legacyIncidentFeatures: incident.value,
|
|
321
|
+
legacyAlertFeatures: alert.value,
|
|
322
|
+
});
|
|
323
|
+
const call: CapturedQuery = calls[0]!;
|
|
324
|
+
|
|
325
|
+
expect(call.params).toHaveLength(lane.expectedParamCount);
|
|
326
|
+
expect(highestPlaceholder(call.sql)).toBe(lane.expectedParamCount);
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
test("omitted legacy lists default to empty arrays and are still bound", async () => {
|
|
333
|
+
const calls: Array<CapturedQuery> = captureQueries();
|
|
334
|
+
|
|
335
|
+
await LlmLogService.getTotalTokensUsedSince({
|
|
336
|
+
projectId: ObjectID.generate(),
|
|
337
|
+
since: SINCE,
|
|
338
|
+
features: AUTONOMOUS_AI_FEATURES,
|
|
339
|
+
incidentId: ObjectID.generate(),
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
expect(calls[0]!.params).toHaveLength(4);
|
|
343
|
+
expect(calls[0]!.params[3]).toEqual([]);
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
test("omitted legacy lists still bind both slots on the subjectless lane", async () => {
|
|
347
|
+
const calls: Array<CapturedQuery> = captureQueries();
|
|
348
|
+
|
|
349
|
+
await LlmLogService.getTotalTokensUsedSince({
|
|
350
|
+
projectId: ObjectID.generate(),
|
|
351
|
+
since: SINCE,
|
|
352
|
+
features: AUTONOMOUS_AI_FEATURES,
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
expect(calls[0]!.params).toHaveLength(5);
|
|
356
|
+
expect(calls[0]!.params[3]).toEqual([]);
|
|
357
|
+
expect(calls[0]!.params[4]).toEqual([]);
|
|
358
|
+
});
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
/*
|
|
362
|
+
* Fixing the parameter counts must not blur the lanes: an incident aggregate
|
|
363
|
+
* that started counting alert spend would silently halve both budgets.
|
|
364
|
+
*/
|
|
365
|
+
describe("subject lane isolation survives the parameter fix", () => {
|
|
366
|
+
test("incident lane never binds alert legacy features", async () => {
|
|
367
|
+
const { calls } = await aggregate({
|
|
368
|
+
subject: { incidentId: ObjectID.generate() },
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
expect(calls[0]!.params).not.toContainEqual(ALERT_LEGACY);
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
test("alert lane never binds incident legacy features", async () => {
|
|
375
|
+
const { calls } = await aggregate({
|
|
376
|
+
subject: { alertId: ObjectID.generate() },
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
expect(calls[0]!.params).not.toContainEqual(INCIDENT_LEGACY);
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
test("incident lane matches incident-linked rows and incident run membership", async () => {
|
|
383
|
+
const { calls } = await aggregate({
|
|
384
|
+
subject: { incidentId: ObjectID.generate() },
|
|
385
|
+
});
|
|
386
|
+
const sql: string = calls[0]!.sql;
|
|
387
|
+
|
|
388
|
+
expect(sql).toContain(
|
|
389
|
+
'"log"."incidentId" IS NOT NULL AND "log"."alertId" IS NULL',
|
|
390
|
+
);
|
|
391
|
+
expect(sql).toContain('"run"."triggeredByIncidentId" IS NOT NULL');
|
|
392
|
+
expect(sql).not.toContain('"run"."triggeredByAlertId" IS NOT NULL');
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
test("alert lane matches alert-linked rows and alert run membership", async () => {
|
|
396
|
+
const { calls } = await aggregate({
|
|
397
|
+
subject: { alertId: ObjectID.generate() },
|
|
398
|
+
});
|
|
399
|
+
const sql: string = calls[0]!.sql;
|
|
400
|
+
|
|
401
|
+
expect(sql).toContain(
|
|
402
|
+
'"log"."incidentId" IS NULL AND "log"."alertId" IS NOT NULL',
|
|
403
|
+
);
|
|
404
|
+
expect(sql).toContain('"run"."triggeredByAlertId" IS NOT NULL');
|
|
405
|
+
expect(sql).not.toContain('"run"."triggeredByIncidentId" IS NOT NULL');
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
test("subjectless lane excludes both lanes' rows and run membership", async () => {
|
|
409
|
+
const { calls } = await aggregate({ subject: {} });
|
|
410
|
+
const sql: string = calls[0]!.sql;
|
|
411
|
+
|
|
412
|
+
expect(sql).toContain(
|
|
413
|
+
'"log"."incidentId" IS NULL AND "log"."alertId" IS NULL',
|
|
414
|
+
);
|
|
415
|
+
expect(sql).toContain('NOT ("log"."feature" = ANY($4))');
|
|
416
|
+
expect(sql).toContain('NOT ("log"."feature" = ANY($5))');
|
|
417
|
+
expect(sql).toContain('NOT (EXISTS (SELECT 1 FROM "AIRun" AS "run"');
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
test.each(lanes())(
|
|
421
|
+
"$name lane always scopes to the project, the window and soft-delete",
|
|
422
|
+
async (lane: Lane) => {
|
|
423
|
+
const { calls } = await aggregate({ subject: lane.subject });
|
|
424
|
+
const sql: string = calls[0]!.sql;
|
|
425
|
+
|
|
426
|
+
expect(sql).toContain('"log"."projectId" = $1');
|
|
427
|
+
expect(sql).toContain('"log"."createdAt" >= $2');
|
|
428
|
+
expect(sql).toContain('"log"."feature" = ANY($3)');
|
|
429
|
+
expect(sql).toContain('"log"."deletedAt" IS NULL');
|
|
430
|
+
},
|
|
431
|
+
);
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
describe("guard clauses run before any SQL is issued", () => {
|
|
435
|
+
test("an empty feature list short-circuits to zero without querying", async () => {
|
|
436
|
+
const calls: Array<CapturedQuery> = captureQueries();
|
|
437
|
+
|
|
438
|
+
const total: number = await LlmLogService.getTotalTokensUsedSince({
|
|
439
|
+
projectId: ObjectID.generate(),
|
|
440
|
+
since: SINCE,
|
|
441
|
+
features: [],
|
|
442
|
+
incidentId: ObjectID.generate(),
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
expect(total).toBe(0);
|
|
446
|
+
expect(calls).toHaveLength(0);
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
test("selecting both lanes is rejected before the query", async () => {
|
|
450
|
+
const calls: Array<CapturedQuery> = captureQueries();
|
|
451
|
+
|
|
452
|
+
await expect(
|
|
453
|
+
LlmLogService.getTotalTokensUsedSince({
|
|
454
|
+
projectId: ObjectID.generate(),
|
|
455
|
+
since: SINCE,
|
|
456
|
+
features: AUTONOMOUS_AI_FEATURES,
|
|
457
|
+
incidentId: ObjectID.generate(),
|
|
458
|
+
alertId: ObjectID.generate(),
|
|
459
|
+
}),
|
|
460
|
+
).rejects.toThrow(/both the incident and alert lanes/);
|
|
461
|
+
|
|
462
|
+
expect(calls).toHaveLength(0);
|
|
463
|
+
});
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
describe("aggregate result parsing", () => {
|
|
467
|
+
test.each([
|
|
468
|
+
["a string sum, as SUM() returns over the wire", "17", 17],
|
|
469
|
+
["a numeric sum", 42, 42],
|
|
470
|
+
["a null sum", null, 0],
|
|
471
|
+
["a zero sum", "0", 0],
|
|
472
|
+
])(
|
|
473
|
+
"reads %s",
|
|
474
|
+
async (_name: string, total: string | number | null, expected: number) => {
|
|
475
|
+
const { total: parsed } = await aggregate({
|
|
476
|
+
subject: { incidentId: ObjectID.generate() },
|
|
477
|
+
rows: [{ total }],
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
expect(parsed).toBe(expected);
|
|
481
|
+
},
|
|
482
|
+
);
|
|
483
|
+
|
|
484
|
+
test("an empty result set counts as zero tokens", async () => {
|
|
485
|
+
const { total } = await aggregate({
|
|
486
|
+
subject: { incidentId: ObjectID.generate() },
|
|
487
|
+
rows: [],
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
expect(total).toBe(0);
|
|
491
|
+
});
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
/*
|
|
495
|
+
* The end-to-end shape of the reported bug: a project with a POSITIVE incident
|
|
496
|
+
* ceiling. Before the fix the aggregate threw, InvestigationQueue.enqueue()
|
|
497
|
+
* swallowed it, and no investigation was ever created — so a project that
|
|
498
|
+
* responsibly capped its spend lost the feature entirely.
|
|
499
|
+
*/
|
|
500
|
+
describe("capped lanes report a real budget instead of throwing", () => {
|
|
501
|
+
function projectWithLimits(data: {
|
|
502
|
+
subjectless?: number | undefined;
|
|
503
|
+
incident?: number | undefined;
|
|
504
|
+
alert?: number | undefined;
|
|
505
|
+
}): Project {
|
|
506
|
+
return {
|
|
507
|
+
id: ObjectID.generate(),
|
|
508
|
+
aiDailyAutonomousTokenLimit: data.subjectless,
|
|
509
|
+
incidentAiDailyAutonomousTokenLimit: data.incident,
|
|
510
|
+
alertAiDailyAutonomousTokenLimit: data.alert,
|
|
511
|
+
} as unknown as Project;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
test("a positive incident ceiling under budget is not exhausted", async () => {
|
|
515
|
+
jest
|
|
516
|
+
.spyOn(ProjectService, "findOneById")
|
|
517
|
+
.mockResolvedValue(projectWithLimits({ incident: 700_000 }));
|
|
518
|
+
const calls: Array<CapturedQuery> = captureQueries([{ total: "1200" }]);
|
|
519
|
+
|
|
520
|
+
const status: AutonomousBudgetStatus =
|
|
521
|
+
await AIService.getAutonomousDailyBudgetStatus(ObjectID.generate(), {
|
|
522
|
+
incidentId: ObjectID.generate(),
|
|
523
|
+
});
|
|
524
|
+
|
|
525
|
+
expect(status.exhausted).toBe(false);
|
|
526
|
+
expect(status.limitInTokens).toBe(700_000);
|
|
527
|
+
expect(status.usedTokensToday).toBe(1200);
|
|
528
|
+
expect(calls).toHaveLength(1);
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
test("a positive incident ceiling that is spent is exhausted", async () => {
|
|
532
|
+
jest
|
|
533
|
+
.spyOn(ProjectService, "findOneById")
|
|
534
|
+
.mockResolvedValue(projectWithLimits({ incident: 700_000 }));
|
|
535
|
+
captureQueries([{ total: "700000" }]);
|
|
536
|
+
|
|
537
|
+
const status: AutonomousBudgetStatus =
|
|
538
|
+
await AIService.getAutonomousDailyBudgetStatus(ObjectID.generate(), {
|
|
539
|
+
incidentId: ObjectID.generate(),
|
|
540
|
+
});
|
|
541
|
+
|
|
542
|
+
expect(status.exhausted).toBe(true);
|
|
543
|
+
expect(status.usedTokensToday).toBe(700_000);
|
|
544
|
+
});
|
|
545
|
+
|
|
546
|
+
test("a positive alert ceiling under budget is not exhausted", async () => {
|
|
547
|
+
jest
|
|
548
|
+
.spyOn(ProjectService, "findOneById")
|
|
549
|
+
.mockResolvedValue(projectWithLimits({ alert: 500_000 }));
|
|
550
|
+
const calls: Array<CapturedQuery> = captureQueries([{ total: "9" }]);
|
|
551
|
+
|
|
552
|
+
const status: AutonomousBudgetStatus =
|
|
553
|
+
await AIService.getAutonomousDailyBudgetStatus(ObjectID.generate(), {
|
|
554
|
+
alertId: ObjectID.generate(),
|
|
555
|
+
});
|
|
556
|
+
|
|
557
|
+
expect(status.exhausted).toBe(false);
|
|
558
|
+
expect(status.limitInTokens).toBe(500_000);
|
|
559
|
+
expect(status.usedTokensToday).toBe(9);
|
|
560
|
+
expect(calls).toHaveLength(1);
|
|
561
|
+
});
|
|
562
|
+
|
|
563
|
+
test("a positive subjectless ceiling under budget is not exhausted", async () => {
|
|
564
|
+
jest
|
|
565
|
+
.spyOn(ProjectService, "findOneById")
|
|
566
|
+
.mockResolvedValue(projectWithLimits({ subjectless: 250_000 }));
|
|
567
|
+
const calls: Array<CapturedQuery> = captureQueries([{ total: "10" }]);
|
|
568
|
+
|
|
569
|
+
const status: AutonomousBudgetStatus =
|
|
570
|
+
await AIService.getAutonomousDailyBudgetStatus(ObjectID.generate());
|
|
571
|
+
|
|
572
|
+
expect(status.exhausted).toBe(false);
|
|
573
|
+
expect(status.usedTokensToday).toBe(10);
|
|
574
|
+
expect(calls).toHaveLength(1);
|
|
575
|
+
});
|
|
576
|
+
|
|
577
|
+
test("the incident lane's aggregate is bindable end to end", async () => {
|
|
578
|
+
jest
|
|
579
|
+
.spyOn(ProjectService, "findOneById")
|
|
580
|
+
.mockResolvedValue(projectWithLimits({ incident: 700_000 }));
|
|
581
|
+
const calls: Array<CapturedQuery> = captureQueries();
|
|
582
|
+
|
|
583
|
+
await AIService.getAutonomousDailyBudgetStatus(ObjectID.generate(), {
|
|
584
|
+
incidentId: ObjectID.generate(),
|
|
585
|
+
});
|
|
586
|
+
|
|
587
|
+
const call: CapturedQuery = calls[0]!;
|
|
588
|
+
expect(() => {
|
|
589
|
+
return assertBindable(call.sql, call.params);
|
|
590
|
+
}).not.toThrow();
|
|
591
|
+
expect(call.params).toHaveLength(4);
|
|
592
|
+
});
|
|
593
|
+
|
|
594
|
+
test("the alert lane's aggregate is bindable end to end", async () => {
|
|
595
|
+
jest
|
|
596
|
+
.spyOn(ProjectService, "findOneById")
|
|
597
|
+
.mockResolvedValue(projectWithLimits({ alert: 500_000 }));
|
|
598
|
+
const calls: Array<CapturedQuery> = captureQueries();
|
|
599
|
+
|
|
600
|
+
await AIService.getAutonomousDailyBudgetStatus(ObjectID.generate(), {
|
|
601
|
+
alertId: ObjectID.generate(),
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
const call: CapturedQuery = calls[0]!;
|
|
605
|
+
expect(() => {
|
|
606
|
+
return assertBindable(call.sql, call.params);
|
|
607
|
+
}).not.toThrow();
|
|
608
|
+
expect(call.params).toHaveLength(4);
|
|
609
|
+
});
|
|
610
|
+
|
|
611
|
+
test("the incident lane carries AIService's real legacy feature list", async () => {
|
|
612
|
+
jest
|
|
613
|
+
.spyOn(ProjectService, "findOneById")
|
|
614
|
+
.mockResolvedValue(projectWithLimits({ incident: 700_000 }));
|
|
615
|
+
const calls: Array<CapturedQuery> = captureQueries();
|
|
616
|
+
|
|
617
|
+
await AIService.getAutonomousDailyBudgetStatus(ObjectID.generate(), {
|
|
618
|
+
incidentId: ObjectID.generate(),
|
|
619
|
+
});
|
|
620
|
+
|
|
621
|
+
const legacy: Array<string> = calls[0]!.params[3] as Array<string>;
|
|
622
|
+
expect(legacy).toContain(AI_INCIDENT_INVESTIGATION_FEATURE);
|
|
623
|
+
expect(legacy).not.toContain(AI_ALERT_INVESTIGATION_FEATURE);
|
|
624
|
+
});
|
|
625
|
+
|
|
626
|
+
test("the alert lane carries AIService's real legacy feature list", async () => {
|
|
627
|
+
jest
|
|
628
|
+
.spyOn(ProjectService, "findOneById")
|
|
629
|
+
.mockResolvedValue(projectWithLimits({ alert: 500_000 }));
|
|
630
|
+
const calls: Array<CapturedQuery> = captureQueries();
|
|
631
|
+
|
|
632
|
+
await AIService.getAutonomousDailyBudgetStatus(ObjectID.generate(), {
|
|
633
|
+
alertId: ObjectID.generate(),
|
|
634
|
+
});
|
|
635
|
+
|
|
636
|
+
const legacy: Array<string> = calls[0]!.params[3] as Array<string>;
|
|
637
|
+
expect(legacy).toContain(AI_ALERT_INVESTIGATION_FEATURE);
|
|
638
|
+
expect(legacy).not.toContain(AI_INCIDENT_INVESTIGATION_FEATURE);
|
|
639
|
+
});
|
|
640
|
+
});
|