@oneuptime/common 12.0.14 → 12.0.15
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/GlobalOidc.ts +20 -0
- package/Models/DatabaseModels/GlobalSso.ts +20 -0
- package/Models/DatabaseModels/MarketingConversion.ts +19 -9
- package/Models/DatabaseModels/NetworkDeviceLinkRule.ts +46 -0
- package/Models/DatabaseModels/StatusPage.ts +7 -4
- package/Server/API/StatusPageAPI.ts +29 -4
- package/Server/EnvironmentConfig.ts +7 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1787800000000-AddScopeToNetworkDeviceLinkRule.ts +19 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1787900000000-AddRestrictToAttachedProjectsToGlobalSso.ts +40 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +4 -0
- package/Server/Middleware/SlackAuthorization.ts +43 -6
- package/Server/Middleware/UserAuthorization.ts +236 -26
- package/Server/Middleware/WhatsAppAuthorization.ts +43 -3
- package/Server/Services/GlobalOidcProjectService.ts +126 -1
- package/Server/Services/GlobalOidcService.ts +132 -0
- package/Server/Services/GlobalSsoProjectService.ts +126 -1
- package/Server/Services/GlobalSsoService.ts +132 -0
- package/Server/Services/UserEmailService.ts +4 -4
- package/Server/Utils/CronTab.ts +73 -0
- package/Server/Utils/GlobalSsoAuthorization.ts +175 -0
- package/Server/Utils/Marketing/ConversionUploadProvider.ts +35 -5
- package/Server/Utils/Marketing/Providers/GoogleAds.ts +1 -1
- package/Server/Utils/Marketing/Providers/LinkedIn.ts +1 -1
- package/Server/Utils/Marketing/Providers/Meta.ts +1 -1
- package/Server/Utils/Marketing/Providers/MicrosoftAds.ts +1 -1
- package/Server/Utils/Marketing/Providers/Reddit.ts +1 -1
- package/Server/Utils/Monitor/Criteria/APIRequestCriteria.ts +48 -33
- package/Server/Utils/Monitor/Criteria/DnsMonitorCriteria.ts +35 -30
- package/Server/Utils/Monitor/Criteria/DomainMonitorCriteria.ts +33 -28
- package/Server/Utils/Monitor/Criteria/EvaluateOverTime.ts +475 -47
- package/Server/Utils/Monitor/Criteria/ExternalStatusPageMonitorCriteria.ts +35 -30
- package/Server/Utils/Monitor/Criteria/IncomingRequestCriteria.ts +34 -28
- package/Server/Utils/Monitor/Criteria/SSLMonitorCriteria.ts +33 -28
- package/Server/Utils/Monitor/Criteria/ServerMonitorCriteria.ts +69 -40
- package/Server/Utils/Monitor/Criteria/SnmpMonitorCriteria.ts +35 -29
- package/Server/Utils/Monitor/MonitorCriteriaEvaluator.ts +8 -0
- package/Server/Utils/Monitor/MonitorResource.ts +161 -21
- package/Server/Utils/StatusPageContentSecurityPolicy.ts +56 -0
- package/Server/Utils/StatusPageCustomizationAccess.ts +117 -0
- package/Tests/Server/API/StatusPageCustomizationOrigin.test.ts +348 -0
- package/Tests/Server/Infrastructure/Postgres/FixTotpOtpUrlAlgorithmMigration.test.ts +9 -59
- package/Tests/Server/Infrastructure/Postgres/SchemaMigrationsOrdering.test.ts +227 -0
- package/Tests/Server/Middleware/SlackAuthorization.test.ts +149 -0
- package/Tests/Server/Middleware/UserAuthorization.test.ts +27 -13
- package/Tests/Server/Middleware/UserAuthorizationGlobalSsoGovernance.test.ts +1455 -0
- package/Tests/Server/Middleware/UserAuthorizationSSOProvider.test.ts +91 -2
- package/Tests/Server/Middleware/WhatsAppAuthorization.test.ts +233 -0
- package/Tests/Server/Services/DiscoveryScanClaimHookFreeSafety.test.ts +21 -8
- package/Tests/Server/Services/GlobalSsoProviderAuthorization.test.ts +1242 -0
- package/Tests/Server/Services/MonitorResourceProbeAgreementHydration.test.ts +459 -0
- package/Tests/Server/Services/MonitorResourceProbeAgreementReuse.test.ts +10 -0
- package/Tests/Server/Types/AnalyticsDatabase/AggregateUtilBucketTimestamp.test.ts +416 -0
- package/Tests/Server/Types/Database/QueryHelperManyToManyAndEmptyValues.test.ts +551 -0
- package/Tests/Server/Types/Database/SelectUtil.test.ts +348 -0
- package/Tests/Server/Utils/CronTab.test.ts +116 -0
- package/Tests/Server/Utils/GlobalSSOToken.test.ts +581 -0
- package/Tests/Server/Utils/GlobalSsoAuthorization.test.ts +1401 -0
- package/Tests/Server/Utils/Marketing/AdUploadableConversionTypes.test.ts +254 -0
- package/Tests/Server/Utils/Marketing/ConversionUploadProvider.test.ts +1 -1
- package/Tests/Server/Utils/Monitor/Criteria/APIRequestCriteriaEvaluateOverTime.test.ts +508 -0
- package/Tests/Server/Utils/Monitor/Criteria/APIRequestCriteriaPortTimings.test.ts +30 -9
- package/Tests/Server/Utils/Monitor/Criteria/EvaluateOverTime.test.ts +987 -0
- package/Tests/Server/Utils/Monitor/Criteria/ExternalStatusPageMonitorCriteria.test.ts +916 -0
- package/Tests/Server/Utils/Monitor/Criteria/ProfileMonitorCriteria.test.ts +382 -0
- package/Tests/Server/Utils/Monitor/Criteria/ServerMonitorCriteria.test.ts +1219 -0
- package/Tests/Server/Utils/Monitor/MonitorResourceIngestedStepSelection.test.ts +86 -0
- package/Tests/Server/Utils/StatusPageContentSecurityPolicy.test.ts +136 -0
- package/Tests/Server/Utils/StatusPageCustomizationAccess.test.ts +223 -0
- package/Tests/Types/Monitor/MonitorStepConfigHelpers.test.ts +165 -0
- package/Tests/Types/NetworkDevice/NetworkDeviceLinkRuleScope.test.ts +215 -0
- package/Tests/UI/Components/Forms/FormSubmitAnalytics.test.tsx +206 -0
- package/Tests/UI/Utils/AIChatExport/ConversationMarkdown.test.ts +774 -0
- package/Tests/UI/Utils/AIChatExport/MarkdownBlocks.test.ts +791 -0
- package/Tests/UI/Utils/AIChatExport/MarkdownSafety.test.ts +544 -0
- package/Tests/Utils/Analytics.test.ts +187 -0
- package/Tests/Utils/Monitor/MonitorMetricType.test.ts +80 -0
- package/Tests/Utils/Monitor/NetworkDeviceLinkRuleUtil.test.ts +1291 -0
- package/Tests/Utils/NetworkDiscovery/UnclaimedScanDiagnosis.test.ts +214 -0
- package/Types/Marketing/MarketingConversion.ts +12 -0
- package/Types/Monitor/CriteriaFilter.ts +9 -0
- package/Types/NetworkDevice/NetworkDeviceLinkRuleScope.ts +51 -0
- package/UI/Components/FormModal/BasicFormModal.tsx +12 -0
- package/UI/Components/Forms/BasicForm.tsx +13 -1
- package/UI/Components/Forms/BasicModelForm.tsx +2 -1
- package/UI/Components/Forms/ModelForm.tsx +10 -1
- package/UI/Components/Forms/Utils/FormAnalyticsName.ts +39 -0
- package/UI/Components/ModelFormModal/ModelFormModal.tsx +6 -1
- package/Utils/Analytics.ts +55 -2
- package/Utils/Monitor/MonitorMetricType.ts +40 -0
- package/Utils/Monitor/NetworkDeviceLinkRuleUtil.ts +587 -37
- package/Utils/NetworkDiscovery/UnclaimedScanDiagnosis.ts +115 -0
- package/build/dist/Models/DatabaseModels/GlobalOidc.js +21 -0
- package/build/dist/Models/DatabaseModels/GlobalOidc.js.map +1 -1
- package/build/dist/Models/DatabaseModels/GlobalSso.js +21 -0
- package/build/dist/Models/DatabaseModels/GlobalSso.js.map +1 -1
- package/build/dist/Models/DatabaseModels/MarketingConversion.js +20 -10
- package/build/dist/Models/DatabaseModels/MarketingConversion.js.map +1 -1
- package/build/dist/Models/DatabaseModels/NetworkDeviceLinkRule.js +47 -0
- package/build/dist/Models/DatabaseModels/NetworkDeviceLinkRule.js.map +1 -1
- package/build/dist/Models/DatabaseModels/StatusPage.js +4 -4
- package/build/dist/Models/DatabaseModels/StatusPage.js.map +1 -1
- package/build/dist/Server/API/StatusPageAPI.js +25 -4
- package/build/dist/Server/API/StatusPageAPI.js.map +1 -1
- package/build/dist/Server/EnvironmentConfig.js +6 -0
- package/build/dist/Server/EnvironmentConfig.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787800000000-AddScopeToNetworkDeviceLinkRule.js +12 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787800000000-AddScopeToNetworkDeviceLinkRule.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787900000000-AddRestrictToAttachedProjectsToGlobalSso.js +29 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787900000000-AddRestrictToAttachedProjectsToGlobalSso.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +4 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
- package/build/dist/Server/Middleware/SlackAuthorization.js +28 -4
- package/build/dist/Server/Middleware/SlackAuthorization.js.map +1 -1
- package/build/dist/Server/Middleware/UserAuthorization.js +197 -22
- package/build/dist/Server/Middleware/UserAuthorization.js.map +1 -1
- package/build/dist/Server/Middleware/WhatsAppAuthorization.js +32 -2
- package/build/dist/Server/Middleware/WhatsAppAuthorization.js.map +1 -1
- package/build/dist/Server/Services/GlobalOidcProjectService.js +105 -0
- package/build/dist/Server/Services/GlobalOidcProjectService.js.map +1 -1
- package/build/dist/Server/Services/GlobalOidcService.js +127 -0
- package/build/dist/Server/Services/GlobalOidcService.js.map +1 -1
- package/build/dist/Server/Services/GlobalSsoProjectService.js +105 -0
- package/build/dist/Server/Services/GlobalSsoProjectService.js.map +1 -1
- package/build/dist/Server/Services/GlobalSsoService.js +127 -0
- package/build/dist/Server/Services/GlobalSsoService.js.map +1 -1
- package/build/dist/Server/Services/UserEmailService.js +4 -4
- package/build/dist/Server/Utils/CronTab.js +66 -0
- package/build/dist/Server/Utils/CronTab.js.map +1 -1
- package/build/dist/Server/Utils/GlobalSsoAuthorization.js +112 -0
- package/build/dist/Server/Utils/GlobalSsoAuthorization.js.map +1 -0
- package/build/dist/Server/Utils/Marketing/ConversionUploadProvider.js +17 -1
- package/build/dist/Server/Utils/Marketing/ConversionUploadProvider.js.map +1 -1
- package/build/dist/Server/Utils/Marketing/Providers/GoogleAds.js +1 -1
- package/build/dist/Server/Utils/Marketing/Providers/GoogleAds.js.map +1 -1
- package/build/dist/Server/Utils/Marketing/Providers/LinkedIn.js +1 -1
- package/build/dist/Server/Utils/Marketing/Providers/LinkedIn.js.map +1 -1
- package/build/dist/Server/Utils/Marketing/Providers/Meta.js +1 -1
- package/build/dist/Server/Utils/Marketing/Providers/Meta.js.map +1 -1
- package/build/dist/Server/Utils/Marketing/Providers/MicrosoftAds.js +1 -1
- package/build/dist/Server/Utils/Marketing/Providers/MicrosoftAds.js.map +1 -1
- package/build/dist/Server/Utils/Marketing/Providers/Reddit.js +1 -1
- package/build/dist/Server/Utils/Marketing/Providers/Reddit.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/Criteria/APIRequestCriteria.js +37 -35
- package/build/dist/Server/Utils/Monitor/Criteria/APIRequestCriteria.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/Criteria/DnsMonitorCriteria.js +21 -25
- package/build/dist/Server/Utils/Monitor/Criteria/DnsMonitorCriteria.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/Criteria/DomainMonitorCriteria.js +20 -22
- package/build/dist/Server/Utils/Monitor/Criteria/DomainMonitorCriteria.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/Criteria/EvaluateOverTime.js +370 -42
- package/build/dist/Server/Utils/Monitor/Criteria/EvaluateOverTime.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/Criteria/ExternalStatusPageMonitorCriteria.js +21 -25
- package/build/dist/Server/Utils/Monitor/Criteria/ExternalStatusPageMonitorCriteria.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/Criteria/IncomingRequestCriteria.js +21 -25
- package/build/dist/Server/Utils/Monitor/Criteria/IncomingRequestCriteria.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/Criteria/SSLMonitorCriteria.js +21 -25
- package/build/dist/Server/Utils/Monitor/Criteria/SSLMonitorCriteria.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/Criteria/ServerMonitorCriteria.js +64 -52
- package/build/dist/Server/Utils/Monitor/Criteria/ServerMonitorCriteria.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/Criteria/SnmpMonitorCriteria.js +25 -29
- package/build/dist/Server/Utils/Monitor/Criteria/SnmpMonitorCriteria.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js +8 -0
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorResource.js +123 -23
- package/build/dist/Server/Utils/Monitor/MonitorResource.js.map +1 -1
- package/build/dist/Server/Utils/StatusPageContentSecurityPolicy.js +39 -0
- package/build/dist/Server/Utils/StatusPageContentSecurityPolicy.js.map +1 -0
- package/build/dist/Server/Utils/StatusPageCustomizationAccess.js +85 -0
- package/build/dist/Server/Utils/StatusPageCustomizationAccess.js.map +1 -0
- package/build/dist/Types/Marketing/MarketingConversion.js +10 -0
- package/build/dist/Types/Marketing/MarketingConversion.js.map +1 -1
- package/build/dist/Types/Monitor/CriteriaFilter.js +1 -0
- package/build/dist/Types/Monitor/CriteriaFilter.js.map +1 -1
- package/build/dist/Types/NetworkDevice/NetworkDeviceLinkRuleScope.js +46 -0
- package/build/dist/Types/NetworkDevice/NetworkDeviceLinkRuleScope.js.map +1 -0
- package/build/dist/UI/Components/FormModal/BasicFormModal.js +2 -1
- package/build/dist/UI/Components/FormModal/BasicFormModal.js.map +1 -1
- package/build/dist/UI/Components/Forms/BasicForm.js +10 -1
- package/build/dist/UI/Components/Forms/BasicForm.js.map +1 -1
- package/build/dist/UI/Components/Forms/BasicModelForm.js +2 -1
- package/build/dist/UI/Components/Forms/BasicModelForm.js.map +1 -1
- package/build/dist/UI/Components/Forms/ModelForm.js +4 -1
- package/build/dist/UI/Components/Forms/ModelForm.js.map +1 -1
- package/build/dist/UI/Components/Forms/Utils/FormAnalyticsName.js +33 -0
- package/build/dist/UI/Components/Forms/Utils/FormAnalyticsName.js.map +1 -0
- package/build/dist/UI/Components/ModelFormModal/ModelFormModal.js +2 -1
- package/build/dist/UI/Components/ModelFormModal/ModelFormModal.js.map +1 -1
- package/build/dist/Utils/Analytics.js +50 -2
- package/build/dist/Utils/Analytics.js.map +1 -1
- package/build/dist/Utils/Monitor/MonitorMetricType.js +37 -0
- package/build/dist/Utils/Monitor/MonitorMetricType.js.map +1 -1
- package/build/dist/Utils/Monitor/NetworkDeviceLinkRuleUtil.js +318 -37
- package/build/dist/Utils/Monitor/NetworkDeviceLinkRuleUtil.js.map +1 -1
- package/build/dist/Utils/NetworkDiscovery/UnclaimedScanDiagnosis.js +87 -0
- package/build/dist/Utils/NetworkDiscovery/UnclaimedScanDiagnosis.js.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,774 @@
|
|
|
1
|
+
import { describe, expect, jest, test } from "@jest/globals";
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
* MarkdownSafety pulls in the ESM-only mdast/unified stack, which this repo's
|
|
5
|
+
* jest config does not transform, and it is a separately unit-tested
|
|
6
|
+
* collaborator. Stub it with an identity that tags its input, so these tests
|
|
7
|
+
* can assert the one thing convertConversationToMarkdown owns about it: that
|
|
8
|
+
* assistant prose is routed THROUGH neutralization while the user's own words
|
|
9
|
+
* are not. The tag makes that routing observable without depending on the real
|
|
10
|
+
* neutralizer's output.
|
|
11
|
+
*/
|
|
12
|
+
jest.mock("../../../../UI/Utils/AIChatExport/MarkdownSafety", () => {
|
|
13
|
+
return {
|
|
14
|
+
__esModule: true,
|
|
15
|
+
default: (text: string): string => {
|
|
16
|
+
return `NEUTRALIZED<<${text}>>`;
|
|
17
|
+
},
|
|
18
|
+
};
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
import AIConversationMessage from "../../../../Models/DatabaseModels/AIConversationMessage";
|
|
22
|
+
import AIRun from "../../../../Models/DatabaseModels/AIRun";
|
|
23
|
+
import AIChatMessageRole from "../../../../Types/AI/AIChatMessageRole";
|
|
24
|
+
import AIChatMessageStatus from "../../../../Types/AI/AIChatMessageStatus";
|
|
25
|
+
import {
|
|
26
|
+
AIChatCitation,
|
|
27
|
+
AIChatToolAction,
|
|
28
|
+
AIChatToolActionStatus,
|
|
29
|
+
AIChatWidget,
|
|
30
|
+
AIChatWidgetType,
|
|
31
|
+
} from "../../../../Types/AI/AIChatTypes";
|
|
32
|
+
import OneUptimeDate from "../../../../Types/Date";
|
|
33
|
+
import ObjectID from "../../../../Types/ObjectID";
|
|
34
|
+
import convertConversationToMarkdown, {
|
|
35
|
+
ConversationMarkdownOptions,
|
|
36
|
+
} from "../../../../UI/Utils/AIChatExport/ConversationMarkdown";
|
|
37
|
+
|
|
38
|
+
/*
|
|
39
|
+
* convertConversationToMarkdown turns a persisted AI chat (the ordered list of
|
|
40
|
+
* user/assistant messages plus the cost of the newest answer) into a single
|
|
41
|
+
* shareable markdown document. These tests pin the structural decisions that
|
|
42
|
+
* matter once the file leaves the app:
|
|
43
|
+
*
|
|
44
|
+
* - the document scaffold (title fallback, export stamp, trailing newline);
|
|
45
|
+
* - per-role rendering, including the security-critical asymmetry that the
|
|
46
|
+
* user's own words are kept literal while assistant prose is neutralized;
|
|
47
|
+
* - the terminal-status short circuits (Error / Cancelled) and their
|
|
48
|
+
* precedence over content, widgets and the cost footer;
|
|
49
|
+
* - which turns are dropped as "still generating", and the narrow shape of
|
|
50
|
+
* that skip (InProgress/Pending only, and blind to citations);
|
|
51
|
+
* - the run footer, which must attach to exactly the newest completed answer.
|
|
52
|
+
*/
|
|
53
|
+
|
|
54
|
+
interface MessageFields {
|
|
55
|
+
id?: string | undefined;
|
|
56
|
+
role: AIChatMessageRole;
|
|
57
|
+
status?: AIChatMessageStatus | undefined;
|
|
58
|
+
content?: string | undefined;
|
|
59
|
+
createdAt?: Date | undefined;
|
|
60
|
+
errorMessage?: string | undefined;
|
|
61
|
+
widgets?: Array<AIChatWidget> | undefined;
|
|
62
|
+
toolActions?: Array<AIChatToolAction> | undefined;
|
|
63
|
+
citations?: Array<AIChatCitation> | undefined;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function makeMessage(fields: MessageFields): AIConversationMessage {
|
|
67
|
+
const message: AIConversationMessage = new AIConversationMessage();
|
|
68
|
+
if (fields.id !== undefined) {
|
|
69
|
+
message.id = new ObjectID(fields.id);
|
|
70
|
+
}
|
|
71
|
+
message.role = fields.role;
|
|
72
|
+
if (fields.status !== undefined) {
|
|
73
|
+
message.status = fields.status;
|
|
74
|
+
}
|
|
75
|
+
if (fields.content !== undefined) {
|
|
76
|
+
message.contentInMarkdown = fields.content;
|
|
77
|
+
}
|
|
78
|
+
if (fields.createdAt !== undefined) {
|
|
79
|
+
message.createdAt = fields.createdAt;
|
|
80
|
+
}
|
|
81
|
+
if (fields.errorMessage !== undefined) {
|
|
82
|
+
message.errorMessage = fields.errorMessage;
|
|
83
|
+
}
|
|
84
|
+
if (fields.widgets !== undefined) {
|
|
85
|
+
message.widgets = fields.widgets;
|
|
86
|
+
}
|
|
87
|
+
if (fields.toolActions !== undefined) {
|
|
88
|
+
message.toolActions = fields.toolActions;
|
|
89
|
+
}
|
|
90
|
+
if (fields.citations !== undefined) {
|
|
91
|
+
message.citations = fields.citations;
|
|
92
|
+
}
|
|
93
|
+
return message;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
interface RunFields {
|
|
97
|
+
modelName?: string | undefined;
|
|
98
|
+
totalTokens?: number | undefined;
|
|
99
|
+
totalCostInUSDCents?: number | undefined;
|
|
100
|
+
toolCallCount?: number | undefined;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function makeRun(fields: RunFields): AIRun {
|
|
104
|
+
const run: AIRun = new AIRun();
|
|
105
|
+
if (fields.modelName !== undefined) {
|
|
106
|
+
run.egressManifest = {
|
|
107
|
+
llmCallCount: 1,
|
|
108
|
+
totalTokens: fields.totalTokens || 0,
|
|
109
|
+
toolDataSentToLlm: [],
|
|
110
|
+
modelName: fields.modelName,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
if (fields.totalTokens !== undefined) {
|
|
114
|
+
run.totalTokens = fields.totalTokens;
|
|
115
|
+
}
|
|
116
|
+
if (fields.totalCostInUSDCents !== undefined) {
|
|
117
|
+
run.totalCostInUSDCents = fields.totalCostInUSDCents;
|
|
118
|
+
}
|
|
119
|
+
if (fields.toolCallCount !== undefined) {
|
|
120
|
+
run.toolCallCount = fields.toolCallCount;
|
|
121
|
+
}
|
|
122
|
+
return run;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const EXPORTED_AT: Date = new Date("2026-08-20T10:20:30.000Z");
|
|
126
|
+
const CREATED_AT: Date = new Date("2026-08-20T09:00:00.000Z");
|
|
127
|
+
|
|
128
|
+
function convert(
|
|
129
|
+
messages: Array<AIConversationMessage>,
|
|
130
|
+
overrides: Partial<ConversationMarkdownOptions> = {},
|
|
131
|
+
): string {
|
|
132
|
+
const options: ConversationMarkdownOptions = {
|
|
133
|
+
title: overrides.title !== undefined ? overrides.title : "Session",
|
|
134
|
+
messages: messages,
|
|
135
|
+
latestRun: overrides.latestRun,
|
|
136
|
+
exportedAt:
|
|
137
|
+
overrides.exportedAt !== undefined ? overrides.exportedAt : EXPORTED_AT,
|
|
138
|
+
};
|
|
139
|
+
return convertConversationToMarkdown(options);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function occurrences(haystack: string, needle: string): number {
|
|
143
|
+
return haystack.split(needle).length - 1;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
describe("convertConversationToMarkdown", () => {
|
|
147
|
+
describe("document scaffold", () => {
|
|
148
|
+
test("renders the title, the export stamp, and a divider", () => {
|
|
149
|
+
const doc: string = convert([], { title: "Payment outage" });
|
|
150
|
+
|
|
151
|
+
expect(doc).toContain("# Payment outage");
|
|
152
|
+
expect(doc).toContain(
|
|
153
|
+
`_Exported from OneUptime on ${OneUptimeDate.getDateAsLocalFormattedString(
|
|
154
|
+
EXPORTED_AT,
|
|
155
|
+
)}_`,
|
|
156
|
+
);
|
|
157
|
+
expect(doc).toContain("---");
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
test("falls back to a generic title when the title is empty", () => {
|
|
161
|
+
const doc: string = convert([], { title: "" });
|
|
162
|
+
|
|
163
|
+
expect(doc).toContain("# AI conversation");
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
test("an empty conversation is just the header, no message blocks", () => {
|
|
167
|
+
const doc: string = convert([], { title: "Empty" });
|
|
168
|
+
|
|
169
|
+
expect(doc).not.toContain("## You");
|
|
170
|
+
expect(doc).not.toContain("## AI");
|
|
171
|
+
expect(doc.endsWith("---\n")).toBe(true);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
test("the document always ends in exactly one trailing newline", () => {
|
|
175
|
+
const doc: string = convert([
|
|
176
|
+
makeMessage({
|
|
177
|
+
id: "u1",
|
|
178
|
+
role: AIChatMessageRole.User,
|
|
179
|
+
content: "hello ",
|
|
180
|
+
}),
|
|
181
|
+
]);
|
|
182
|
+
|
|
183
|
+
expect(doc.endsWith("\n")).toBe(true);
|
|
184
|
+
expect(doc.endsWith("\n\n")).toBe(false);
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
describe("user turns", () => {
|
|
189
|
+
test("renders the You heading, the trimmed content, and the timestamp", () => {
|
|
190
|
+
const doc: string = convert([
|
|
191
|
+
makeMessage({
|
|
192
|
+
id: "u1",
|
|
193
|
+
role: AIChatMessageRole.User,
|
|
194
|
+
content: " Why is checkout down? ",
|
|
195
|
+
createdAt: CREATED_AT,
|
|
196
|
+
}),
|
|
197
|
+
]);
|
|
198
|
+
|
|
199
|
+
expect(doc).toContain("## You");
|
|
200
|
+
expect(doc).toContain("## You\n\nWhy is checkout down?\n");
|
|
201
|
+
expect(doc).toContain(
|
|
202
|
+
`_${OneUptimeDate.getDateAsLocalFormattedString(CREATED_AT)}_`,
|
|
203
|
+
);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
test("keeps the user's own markdown literal — no link neutralization", () => {
|
|
207
|
+
const doc: string = convert([
|
|
208
|
+
makeMessage({
|
|
209
|
+
id: "u1",
|
|
210
|
+
role: AIChatMessageRole.User,
|
|
211
|
+
content: "see [dash](http://internal.example/dash)",
|
|
212
|
+
}),
|
|
213
|
+
]);
|
|
214
|
+
|
|
215
|
+
/*
|
|
216
|
+
* The operator's own input is trusted, so the live link syntax survives
|
|
217
|
+
* verbatim and is never sent through the neutralizer. This is the exact
|
|
218
|
+
* opposite of the assistant path.
|
|
219
|
+
*/
|
|
220
|
+
expect(doc).toContain("[dash](http://internal.example/dash)");
|
|
221
|
+
expect(doc).not.toContain("NEUTRALIZED");
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
test("a user turn with no content still emits the heading only", () => {
|
|
225
|
+
const doc: string = convert([
|
|
226
|
+
makeMessage({ id: "u1", role: AIChatMessageRole.User }),
|
|
227
|
+
]);
|
|
228
|
+
|
|
229
|
+
expect(doc).toContain("## You");
|
|
230
|
+
expect(doc).not.toContain("_MMM");
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
test("omits the timestamp line when the message has no createdAt", () => {
|
|
234
|
+
const doc: string = convert([
|
|
235
|
+
makeMessage({
|
|
236
|
+
id: "u1",
|
|
237
|
+
role: AIChatMessageRole.User,
|
|
238
|
+
content: "no date here",
|
|
239
|
+
}),
|
|
240
|
+
]);
|
|
241
|
+
|
|
242
|
+
const afterContent: string = doc.substring(doc.indexOf("no date here"));
|
|
243
|
+
expect(afterContent).not.toContain("_");
|
|
244
|
+
});
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
describe("assistant terminal statuses", () => {
|
|
248
|
+
test("an errored turn renders the error message and nothing else", () => {
|
|
249
|
+
const doc: string = convert([
|
|
250
|
+
makeMessage({
|
|
251
|
+
id: "a1",
|
|
252
|
+
role: AIChatMessageRole.Assistant,
|
|
253
|
+
status: AIChatMessageStatus.Error,
|
|
254
|
+
content: "leaked partial answer",
|
|
255
|
+
errorMessage: "Model timed out",
|
|
256
|
+
widgets: [
|
|
257
|
+
{
|
|
258
|
+
id: "W1",
|
|
259
|
+
type: AIChatWidgetType.StatCards,
|
|
260
|
+
title: "Should not appear",
|
|
261
|
+
data: { stats: [{ label: "Errors", value: 3 }] },
|
|
262
|
+
},
|
|
263
|
+
],
|
|
264
|
+
createdAt: CREATED_AT,
|
|
265
|
+
}),
|
|
266
|
+
]);
|
|
267
|
+
|
|
268
|
+
expect(doc).toContain("> **Error:** Model timed out");
|
|
269
|
+
/*
|
|
270
|
+
* The error branch returns early, so neither the (already-generated)
|
|
271
|
+
* content nor any attached widget is exported, and there is no footer.
|
|
272
|
+
*/
|
|
273
|
+
expect(doc).not.toContain("leaked partial answer");
|
|
274
|
+
expect(doc).not.toContain("Should not appear");
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
test("an errored turn without a message uses the default error text", () => {
|
|
278
|
+
const doc: string = convert([
|
|
279
|
+
makeMessage({
|
|
280
|
+
id: "a1",
|
|
281
|
+
role: AIChatMessageRole.Assistant,
|
|
282
|
+
status: AIChatMessageStatus.Error,
|
|
283
|
+
}),
|
|
284
|
+
]);
|
|
285
|
+
|
|
286
|
+
expect(doc).toContain(
|
|
287
|
+
"> **Error:** Something went wrong generating this response.",
|
|
288
|
+
);
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
test("a cancelled turn renders its finalizer text as a stop marker", () => {
|
|
292
|
+
const doc: string = convert([
|
|
293
|
+
makeMessage({
|
|
294
|
+
id: "a1",
|
|
295
|
+
role: AIChatMessageRole.Assistant,
|
|
296
|
+
status: AIChatMessageStatus.Cancelled,
|
|
297
|
+
content: "Stopped mid-thought.",
|
|
298
|
+
widgets: [
|
|
299
|
+
{
|
|
300
|
+
id: "W1",
|
|
301
|
+
type: AIChatWidgetType.StatCards,
|
|
302
|
+
title: "Hidden widget",
|
|
303
|
+
data: { stats: [{ label: "Errors", value: 3 }] },
|
|
304
|
+
},
|
|
305
|
+
],
|
|
306
|
+
}),
|
|
307
|
+
]);
|
|
308
|
+
|
|
309
|
+
expect(doc).toContain("> _Stopped mid-thought._");
|
|
310
|
+
expect(doc).not.toContain("Hidden widget");
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
test("a cancelled turn with no content falls back to Stopped by user", () => {
|
|
314
|
+
const doc: string = convert([
|
|
315
|
+
makeMessage({
|
|
316
|
+
id: "a1",
|
|
317
|
+
role: AIChatMessageRole.Assistant,
|
|
318
|
+
status: AIChatMessageStatus.Cancelled,
|
|
319
|
+
}),
|
|
320
|
+
]);
|
|
321
|
+
|
|
322
|
+
expect(doc).toContain("> _Stopped by user._");
|
|
323
|
+
});
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
describe("completed assistant turns", () => {
|
|
327
|
+
test("routes assistant prose through the markdown neutralizer", () => {
|
|
328
|
+
const doc: string = convert([
|
|
329
|
+
makeMessage({
|
|
330
|
+
id: "a1",
|
|
331
|
+
role: AIChatMessageRole.Assistant,
|
|
332
|
+
status: AIChatMessageStatus.Completed,
|
|
333
|
+
content: "click [here](http://evil.example/steal)",
|
|
334
|
+
}),
|
|
335
|
+
]);
|
|
336
|
+
|
|
337
|
+
/*
|
|
338
|
+
* Assistant prose is attacker-influenceable, so it must pass through
|
|
339
|
+
* neutralizeAssistantMarkdown. The stub tags whatever it receives, which
|
|
340
|
+
* proves the content took that path rather than being emitted raw.
|
|
341
|
+
*/
|
|
342
|
+
expect(doc).toContain(
|
|
343
|
+
"NEUTRALIZED<<click [here](http://evil.example/steal)>>",
|
|
344
|
+
);
|
|
345
|
+
expect(doc).toContain("## AI");
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
test("renders attached widgets, tool actions and citations in order", () => {
|
|
349
|
+
const widgets: Array<AIChatWidget> = [
|
|
350
|
+
{
|
|
351
|
+
id: "W1",
|
|
352
|
+
type: AIChatWidgetType.StatCards,
|
|
353
|
+
title: "Summary",
|
|
354
|
+
data: { stats: [{ label: "Errors", value: 3 }] },
|
|
355
|
+
},
|
|
356
|
+
];
|
|
357
|
+
const toolActions: Array<AIChatToolAction> = [
|
|
358
|
+
{
|
|
359
|
+
id: "t1",
|
|
360
|
+
toolName: "restart",
|
|
361
|
+
title: "Restart payment-svc",
|
|
362
|
+
arguments: { service: "payment-svc" },
|
|
363
|
+
isMutation: true,
|
|
364
|
+
requiresApproval: true,
|
|
365
|
+
status: AIChatToolActionStatus.Executed,
|
|
366
|
+
},
|
|
367
|
+
];
|
|
368
|
+
const citations: Array<AIChatCitation> = [
|
|
369
|
+
{
|
|
370
|
+
id: "C1",
|
|
371
|
+
toolName: "logs.query",
|
|
372
|
+
label: "Logs for payment-svc",
|
|
373
|
+
queryArguments: { service: "payment-svc" },
|
|
374
|
+
rowCount: 4,
|
|
375
|
+
},
|
|
376
|
+
];
|
|
377
|
+
|
|
378
|
+
const doc: string = convert([
|
|
379
|
+
makeMessage({
|
|
380
|
+
id: "a1",
|
|
381
|
+
role: AIChatMessageRole.Assistant,
|
|
382
|
+
status: AIChatMessageStatus.Completed,
|
|
383
|
+
content: "Here is what I found.",
|
|
384
|
+
widgets: widgets,
|
|
385
|
+
toolActions: toolActions,
|
|
386
|
+
citations: citations,
|
|
387
|
+
}),
|
|
388
|
+
]);
|
|
389
|
+
|
|
390
|
+
expect(doc).toContain("Here is what I found.");
|
|
391
|
+
expect(doc).toContain("#### Summary");
|
|
392
|
+
expect(doc).toContain("| Errors | 3 |");
|
|
393
|
+
expect(doc).toContain("#### Actions");
|
|
394
|
+
expect(doc).toContain("Restart payment-svc");
|
|
395
|
+
expect(doc).toContain("#### Sources");
|
|
396
|
+
expect(doc).toContain("Logs for payment-svc");
|
|
397
|
+
|
|
398
|
+
const bodyIndex: number = doc.indexOf("Here is what I found.");
|
|
399
|
+
const widgetIndex: number = doc.indexOf("#### Summary");
|
|
400
|
+
const actionsIndex: number = doc.indexOf("#### Actions");
|
|
401
|
+
const sourcesIndex: number = doc.indexOf("#### Sources");
|
|
402
|
+
expect(bodyIndex).toBeLessThan(widgetIndex);
|
|
403
|
+
expect(widgetIndex).toBeLessThan(actionsIndex);
|
|
404
|
+
expect(actionsIndex).toBeLessThan(sourcesIndex);
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
test("a completed turn with only a timestamp footer omits the run line", () => {
|
|
408
|
+
const doc: string = convert([
|
|
409
|
+
makeMessage({
|
|
410
|
+
id: "a1",
|
|
411
|
+
role: AIChatMessageRole.Assistant,
|
|
412
|
+
status: AIChatMessageStatus.Completed,
|
|
413
|
+
content: "done",
|
|
414
|
+
createdAt: CREATED_AT,
|
|
415
|
+
}),
|
|
416
|
+
]);
|
|
417
|
+
|
|
418
|
+
expect(doc).toContain(
|
|
419
|
+
`_${OneUptimeDate.getDateAsLocalFormattedString(CREATED_AT)}_`,
|
|
420
|
+
);
|
|
421
|
+
expect(doc).not.toContain("queries");
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
test("a completed turn carrying only citations is not skipped", () => {
|
|
425
|
+
const doc: string = convert([
|
|
426
|
+
makeMessage({
|
|
427
|
+
id: "a1",
|
|
428
|
+
role: AIChatMessageRole.Assistant,
|
|
429
|
+
status: AIChatMessageStatus.Completed,
|
|
430
|
+
citations: [
|
|
431
|
+
{
|
|
432
|
+
id: "C1",
|
|
433
|
+
toolName: "logs.query",
|
|
434
|
+
label: "Logs",
|
|
435
|
+
queryArguments: {},
|
|
436
|
+
rowCount: 0,
|
|
437
|
+
},
|
|
438
|
+
],
|
|
439
|
+
}),
|
|
440
|
+
]);
|
|
441
|
+
|
|
442
|
+
expect(doc).toContain("## AI");
|
|
443
|
+
expect(doc).toContain("#### Sources");
|
|
444
|
+
});
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
describe("skipping still-generating turns", () => {
|
|
448
|
+
test("drops an InProgress assistant that has no content yet", () => {
|
|
449
|
+
const doc: string = convert([
|
|
450
|
+
makeMessage({
|
|
451
|
+
id: "u1",
|
|
452
|
+
role: AIChatMessageRole.User,
|
|
453
|
+
content: "question",
|
|
454
|
+
}),
|
|
455
|
+
makeMessage({
|
|
456
|
+
id: "a1",
|
|
457
|
+
role: AIChatMessageRole.Assistant,
|
|
458
|
+
status: AIChatMessageStatus.InProgress,
|
|
459
|
+
}),
|
|
460
|
+
]);
|
|
461
|
+
|
|
462
|
+
expect(doc).toContain("## You");
|
|
463
|
+
expect(doc).not.toContain("## AI");
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
test("drops a Pending assistant with no content", () => {
|
|
467
|
+
const doc: string = convert([
|
|
468
|
+
makeMessage({
|
|
469
|
+
id: "a1",
|
|
470
|
+
role: AIChatMessageRole.Assistant,
|
|
471
|
+
status: AIChatMessageStatus.Pending,
|
|
472
|
+
}),
|
|
473
|
+
]);
|
|
474
|
+
|
|
475
|
+
expect(doc).not.toContain("## AI");
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
test("keeps an InProgress assistant once it has streamed some content", () => {
|
|
479
|
+
const doc: string = convert([
|
|
480
|
+
makeMessage({
|
|
481
|
+
id: "a1",
|
|
482
|
+
role: AIChatMessageRole.Assistant,
|
|
483
|
+
status: AIChatMessageStatus.InProgress,
|
|
484
|
+
content: "partial but present",
|
|
485
|
+
}),
|
|
486
|
+
]);
|
|
487
|
+
|
|
488
|
+
expect(doc).toContain("## AI");
|
|
489
|
+
expect(doc).toContain("partial but present");
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
test("keeps an InProgress assistant that already has a widget", () => {
|
|
493
|
+
const doc: string = convert([
|
|
494
|
+
makeMessage({
|
|
495
|
+
id: "a1",
|
|
496
|
+
role: AIChatMessageRole.Assistant,
|
|
497
|
+
status: AIChatMessageStatus.InProgress,
|
|
498
|
+
widgets: [
|
|
499
|
+
{
|
|
500
|
+
id: "W1",
|
|
501
|
+
type: AIChatWidgetType.StatCards,
|
|
502
|
+
title: "Early widget",
|
|
503
|
+
data: { stats: [{ label: "Errors", value: 3 }] },
|
|
504
|
+
},
|
|
505
|
+
],
|
|
506
|
+
}),
|
|
507
|
+
]);
|
|
508
|
+
|
|
509
|
+
expect(doc).toContain("## AI");
|
|
510
|
+
expect(doc).toContain("#### Early widget");
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
test("citations alone do not save an InProgress turn from being skipped", () => {
|
|
514
|
+
const doc: string = convert([
|
|
515
|
+
makeMessage({
|
|
516
|
+
id: "u1",
|
|
517
|
+
role: AIChatMessageRole.User,
|
|
518
|
+
content: "question",
|
|
519
|
+
}),
|
|
520
|
+
makeMessage({
|
|
521
|
+
id: "a1",
|
|
522
|
+
role: AIChatMessageRole.Assistant,
|
|
523
|
+
status: AIChatMessageStatus.InProgress,
|
|
524
|
+
citations: [
|
|
525
|
+
{
|
|
526
|
+
id: "C1",
|
|
527
|
+
toolName: "logs.query",
|
|
528
|
+
label: "Logs",
|
|
529
|
+
queryArguments: {},
|
|
530
|
+
rowCount: 2,
|
|
531
|
+
},
|
|
532
|
+
],
|
|
533
|
+
}),
|
|
534
|
+
]);
|
|
535
|
+
|
|
536
|
+
/*
|
|
537
|
+
* The skip condition inspects content, widgets and toolActions but NOT
|
|
538
|
+
* citations, so a turn that has only citations is still dropped.
|
|
539
|
+
*/
|
|
540
|
+
expect(doc).not.toContain("## AI");
|
|
541
|
+
expect(doc).not.toContain("#### Sources");
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
test("a WaitingForApproval turn is rendered, never skipped", () => {
|
|
545
|
+
const doc: string = convert([
|
|
546
|
+
makeMessage({
|
|
547
|
+
id: "a1",
|
|
548
|
+
role: AIChatMessageRole.Assistant,
|
|
549
|
+
status: AIChatMessageStatus.WaitingForApproval,
|
|
550
|
+
toolActions: [
|
|
551
|
+
{
|
|
552
|
+
id: "t1",
|
|
553
|
+
toolName: "restart",
|
|
554
|
+
title: "Restart payment-svc",
|
|
555
|
+
arguments: {},
|
|
556
|
+
isMutation: true,
|
|
557
|
+
requiresApproval: true,
|
|
558
|
+
status: AIChatToolActionStatus.Pending,
|
|
559
|
+
},
|
|
560
|
+
],
|
|
561
|
+
}),
|
|
562
|
+
]);
|
|
563
|
+
|
|
564
|
+
expect(doc).toContain("## AI");
|
|
565
|
+
expect(doc).toContain("#### Actions");
|
|
566
|
+
});
|
|
567
|
+
});
|
|
568
|
+
|
|
569
|
+
describe("run footer", () => {
|
|
570
|
+
test("renders the model, tokens, cost and query count on the newest answer", () => {
|
|
571
|
+
const run: AIRun = makeRun({
|
|
572
|
+
modelName: "gpt-test",
|
|
573
|
+
totalTokens: 1234567,
|
|
574
|
+
totalCostInUSDCents: 12345,
|
|
575
|
+
toolCallCount: 3,
|
|
576
|
+
});
|
|
577
|
+
|
|
578
|
+
const doc: string = convert(
|
|
579
|
+
[
|
|
580
|
+
makeMessage({
|
|
581
|
+
id: "a1",
|
|
582
|
+
role: AIChatMessageRole.Assistant,
|
|
583
|
+
status: AIChatMessageStatus.Completed,
|
|
584
|
+
content: "answer",
|
|
585
|
+
createdAt: CREATED_AT,
|
|
586
|
+
}),
|
|
587
|
+
],
|
|
588
|
+
{ latestRun: run },
|
|
589
|
+
);
|
|
590
|
+
|
|
591
|
+
const tokensText: string = (1234567).toLocaleString();
|
|
592
|
+
const timestamp: string =
|
|
593
|
+
OneUptimeDate.getDateAsLocalFormattedString(CREATED_AT);
|
|
594
|
+
expect(doc).toContain(
|
|
595
|
+
`_${timestamp} · gpt-test · ${tokensText} tokens · $123.4500 · 3 queries_`,
|
|
596
|
+
);
|
|
597
|
+
});
|
|
598
|
+
|
|
599
|
+
test("uses the singular query label for exactly one tool call", () => {
|
|
600
|
+
const doc: string = convert(
|
|
601
|
+
[
|
|
602
|
+
makeMessage({
|
|
603
|
+
id: "a1",
|
|
604
|
+
role: AIChatMessageRole.Assistant,
|
|
605
|
+
status: AIChatMessageStatus.Completed,
|
|
606
|
+
content: "answer",
|
|
607
|
+
}),
|
|
608
|
+
],
|
|
609
|
+
{ latestRun: makeRun({ toolCallCount: 1 }) },
|
|
610
|
+
);
|
|
611
|
+
|
|
612
|
+
expect(doc).toContain("_1 query_");
|
|
613
|
+
});
|
|
614
|
+
|
|
615
|
+
test("defaults an absent tool-call count to zero queries", () => {
|
|
616
|
+
const doc: string = convert(
|
|
617
|
+
[
|
|
618
|
+
makeMessage({
|
|
619
|
+
id: "a1",
|
|
620
|
+
role: AIChatMessageRole.Assistant,
|
|
621
|
+
status: AIChatMessageStatus.Completed,
|
|
622
|
+
content: "answer",
|
|
623
|
+
}),
|
|
624
|
+
],
|
|
625
|
+
{ latestRun: makeRun({}) },
|
|
626
|
+
);
|
|
627
|
+
|
|
628
|
+
expect(doc).toContain("_0 queries_");
|
|
629
|
+
});
|
|
630
|
+
|
|
631
|
+
test("drops zero token and zero cost parts from the footer", () => {
|
|
632
|
+
const doc: string = convert(
|
|
633
|
+
[
|
|
634
|
+
makeMessage({
|
|
635
|
+
id: "a1",
|
|
636
|
+
role: AIChatMessageRole.Assistant,
|
|
637
|
+
status: AIChatMessageStatus.Completed,
|
|
638
|
+
content: "answer",
|
|
639
|
+
}),
|
|
640
|
+
],
|
|
641
|
+
{
|
|
642
|
+
latestRun: makeRun({
|
|
643
|
+
totalTokens: 0,
|
|
644
|
+
totalCostInUSDCents: 0,
|
|
645
|
+
toolCallCount: 2,
|
|
646
|
+
}),
|
|
647
|
+
},
|
|
648
|
+
);
|
|
649
|
+
|
|
650
|
+
expect(doc).toContain("_2 queries_");
|
|
651
|
+
expect(doc).not.toContain("tokens");
|
|
652
|
+
expect(doc).not.toContain("$");
|
|
653
|
+
});
|
|
654
|
+
|
|
655
|
+
test("omits the model name when there is no egress manifest", () => {
|
|
656
|
+
const doc: string = convert(
|
|
657
|
+
[
|
|
658
|
+
makeMessage({
|
|
659
|
+
id: "a1",
|
|
660
|
+
role: AIChatMessageRole.Assistant,
|
|
661
|
+
status: AIChatMessageStatus.Completed,
|
|
662
|
+
content: "answer",
|
|
663
|
+
}),
|
|
664
|
+
],
|
|
665
|
+
{ latestRun: makeRun({ totalTokens: 500, toolCallCount: 1 }) },
|
|
666
|
+
);
|
|
667
|
+
|
|
668
|
+
expect(doc).toContain(`_${(500).toLocaleString()} tokens · 1 query_`);
|
|
669
|
+
});
|
|
670
|
+
|
|
671
|
+
test("attaches the footer to only the last completed assistant", () => {
|
|
672
|
+
const run: AIRun = makeRun({ modelName: "gpt-test", toolCallCount: 1 });
|
|
673
|
+
|
|
674
|
+
const doc: string = convert(
|
|
675
|
+
[
|
|
676
|
+
makeMessage({
|
|
677
|
+
id: "a1",
|
|
678
|
+
role: AIChatMessageRole.Assistant,
|
|
679
|
+
status: AIChatMessageStatus.Completed,
|
|
680
|
+
content: "First answer",
|
|
681
|
+
createdAt: CREATED_AT,
|
|
682
|
+
}),
|
|
683
|
+
makeMessage({
|
|
684
|
+
id: "a2",
|
|
685
|
+
role: AIChatMessageRole.Assistant,
|
|
686
|
+
status: AIChatMessageStatus.Completed,
|
|
687
|
+
content: "Second answer",
|
|
688
|
+
createdAt: CREATED_AT,
|
|
689
|
+
}),
|
|
690
|
+
],
|
|
691
|
+
{ latestRun: run },
|
|
692
|
+
);
|
|
693
|
+
|
|
694
|
+
expect(doc.indexOf("First answer")).toBeLessThan(
|
|
695
|
+
doc.indexOf("Second answer"),
|
|
696
|
+
);
|
|
697
|
+
expect(occurrences(doc, "gpt-test")).toBe(1);
|
|
698
|
+
const footerIndex: number = doc.indexOf("gpt-test");
|
|
699
|
+
expect(footerIndex).toBeGreaterThan(doc.indexOf("Second answer"));
|
|
700
|
+
});
|
|
701
|
+
|
|
702
|
+
test("no run footer appears when there is no completed assistant", () => {
|
|
703
|
+
const doc: string = convert(
|
|
704
|
+
[
|
|
705
|
+
makeMessage({
|
|
706
|
+
id: "u1",
|
|
707
|
+
role: AIChatMessageRole.User,
|
|
708
|
+
content: "question",
|
|
709
|
+
}),
|
|
710
|
+
makeMessage({
|
|
711
|
+
id: "a1",
|
|
712
|
+
role: AIChatMessageRole.Assistant,
|
|
713
|
+
status: AIChatMessageStatus.Error,
|
|
714
|
+
errorMessage: "boom",
|
|
715
|
+
}),
|
|
716
|
+
],
|
|
717
|
+
{ latestRun: makeRun({ modelName: "gpt-test", toolCallCount: 3 }) },
|
|
718
|
+
);
|
|
719
|
+
|
|
720
|
+
expect(doc).not.toContain("gpt-test");
|
|
721
|
+
expect(doc).not.toContain("queries");
|
|
722
|
+
});
|
|
723
|
+
|
|
724
|
+
test("no run footer when latestRun is undefined", () => {
|
|
725
|
+
const doc: string = convert([
|
|
726
|
+
makeMessage({
|
|
727
|
+
id: "a1",
|
|
728
|
+
role: AIChatMessageRole.Assistant,
|
|
729
|
+
status: AIChatMessageStatus.Completed,
|
|
730
|
+
content: "answer",
|
|
731
|
+
createdAt: CREATED_AT,
|
|
732
|
+
}),
|
|
733
|
+
]);
|
|
734
|
+
|
|
735
|
+
expect(doc).not.toContain("queries");
|
|
736
|
+
});
|
|
737
|
+
});
|
|
738
|
+
|
|
739
|
+
describe("whole-conversation ordering", () => {
|
|
740
|
+
test("emits turns in message order with a completed run footer at the end", () => {
|
|
741
|
+
const doc: string = convert(
|
|
742
|
+
[
|
|
743
|
+
makeMessage({
|
|
744
|
+
id: "u1",
|
|
745
|
+
role: AIChatMessageRole.User,
|
|
746
|
+
content: "Why is checkout down?",
|
|
747
|
+
createdAt: CREATED_AT,
|
|
748
|
+
}),
|
|
749
|
+
makeMessage({
|
|
750
|
+
id: "a1",
|
|
751
|
+
role: AIChatMessageRole.Assistant,
|
|
752
|
+
status: AIChatMessageStatus.Completed,
|
|
753
|
+
content: "A dependency was failing.",
|
|
754
|
+
createdAt: CREATED_AT,
|
|
755
|
+
}),
|
|
756
|
+
],
|
|
757
|
+
{
|
|
758
|
+
title: "Checkout incident",
|
|
759
|
+
latestRun: makeRun({ modelName: "gpt-test", toolCallCount: 2 }),
|
|
760
|
+
},
|
|
761
|
+
);
|
|
762
|
+
|
|
763
|
+
const titleIndex: number = doc.indexOf("# Checkout incident");
|
|
764
|
+
const youIndex: number = doc.indexOf("## You");
|
|
765
|
+
const aiIndex: number = doc.indexOf("## AI");
|
|
766
|
+
const footerIndex: number = doc.indexOf("gpt-test");
|
|
767
|
+
|
|
768
|
+
expect(titleIndex).toBeGreaterThanOrEqual(0);
|
|
769
|
+
expect(titleIndex).toBeLessThan(youIndex);
|
|
770
|
+
expect(youIndex).toBeLessThan(aiIndex);
|
|
771
|
+
expect(aiIndex).toBeLessThan(footerIndex);
|
|
772
|
+
});
|
|
773
|
+
});
|
|
774
|
+
});
|