@oneuptime/common 11.5.7 → 11.5.9
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/AIAgentTaskPullRequest.ts +15 -6
- package/Models/DatabaseModels/AIRun.ts +68 -0
- package/Models/DatabaseModels/AIRunEvent.ts +34 -1
- package/Models/DatabaseModels/NetworkDeviceDiscoveryScan.ts +215 -0
- package/Models/DatabaseModels/Project.ts +25 -0
- package/Models/DatabaseModels/StatusPage.ts +42 -0
- package/Server/API/AIAgentTaskAPI.ts +38 -12
- package/Server/API/AIAgentTaskLogAPI.ts +49 -0
- package/Server/API/AIChatAPI.ts +16 -0
- package/Server/API/AIReadinessAPI.ts +84 -0
- package/Server/API/CodeFixRunAPI.ts +190 -57
- package/Server/API/TelemetryExceptionAPI.ts +6 -1
- package/Server/Infrastructure/Postgres/SchemaMigrations/1784105912819-BackfillCodeFixTaskType.ts +41 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1784124919694-AddAITaskNumberAndRunTranscript.ts +95 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1784135099754-AllowNullAiAgentOnPullRequest.ts +52 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1784137457184-AddEnableMcpServerToStatusPage.ts +30 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1784200000000-AddSnmpV3ColumnsToNetworkDeviceDiscoveryScan.ts +49 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +10 -0
- package/Server/Middleware/MasterAdminAuthorization.ts +1 -1
- package/Server/Middleware/ProjectAuthorization.ts +1 -1
- package/Server/Services/AIRunEventService.ts +15 -1
- package/Server/Services/AIRunService.ts +48 -12
- package/Server/Services/LlmProviderService.ts +2 -0
- package/Server/Services/ProjectService.ts +42 -0
- package/Server/Services/StatusPageService.ts +87 -0
- package/Server/Services/TelemetryExceptionService.ts +19 -90
- package/Server/Types/Markdown.ts +9 -9
- package/Server/Types/MarkdownSlugify.ts +47 -0
- package/Server/Utils/AI/AIRunPrivacyFilter.ts +106 -0
- package/Server/Utils/AI/AIRunTranscript.ts +240 -0
- package/Server/Utils/AI/Chat/ChatAgentRunner.ts +9 -0
- package/Server/Utils/AI/Chat/ObservabilityChatPrompt.ts +119 -5
- package/Server/Utils/AI/CodeFix/CodeFixAgentCompletion.ts +75 -0
- package/Server/Utils/AI/CodeFix/CodeFixReadiness.ts +235 -0
- package/Server/Utils/AI/SRE/Insights/FixRouting.ts +2 -3
- package/Server/Utils/AI/Toolbox/CodeTools.ts +883 -0
- package/Server/Utils/AI/Toolbox/CodeWriteTools.ts +519 -0
- package/Server/Utils/AI/Toolbox/Index.ts +27 -0
- package/Server/Utils/AI/Toolbox/ScheduledMaintenanceTools.ts +279 -0
- package/Server/Utils/AnalyticsDatabase/ClickhouseCapacity.ts +19 -1
- package/Server/Utils/CodeRepository/GitHub/GitHub.ts +600 -0
- package/Server/Utils/CodeRepository/StackTraceRepoResolver.ts +7 -2
- package/Server/Utils/LLM/LLMService.ts +397 -18
- package/Server/Utils/Monitor/NetworkDeviceHydrationUtil.ts +4 -11
- package/Tests/Models/StatusPageEnableMcpServer.test.ts +125 -0
- package/Tests/Server/Services/StatusPageServiceMcp.test.ts +223 -0
- package/Tests/Server/Services/TelemetryExceptionAIFixReadiness.test.ts +26 -3
- package/Tests/Server/Services/TelemetryExceptionCodeFixRun.test.ts +12 -0
- package/Tests/Server/Types/Markdown.test.ts +106 -0
- package/Tests/Server/Utils/AI/AIRunPrivacyFilter.test.ts +295 -0
- package/Tests/Server/Utils/AI/AIRunTranscript.test.ts +182 -0
- package/Tests/Server/Utils/AI/CodeFixReadiness.test.ts +411 -0
- package/Tests/Server/Utils/AI/CodeTools.test.ts +596 -0
- package/Tests/Server/Utils/AI/CodeWriteTools.test.ts +475 -0
- package/Tests/Server/Utils/AI/LLMServiceModelCompatibility.test.ts +548 -0
- package/Tests/Server/Utils/AI/ObservabilityChatPrompt.test.ts +142 -0
- package/Tests/Server/Utils/AI/ScheduledMaintenanceTools.test.ts +176 -0
- package/Tests/Server/Utils/AnalyticsDatabase/ClickhouseCapacity.test.ts +63 -0
- package/Tests/Server/Utils/GitHubGetFileContent.test.ts +197 -0
- package/Tests/Server/Utils/Monitor/NetworkDeviceHydrationUtil.test.ts +502 -0
- package/Tests/Types/AI/AIAgentTaskStatus.test.ts +7 -6
- package/Tests/Types/AI/AIChatPageContext.test.ts +0 -0
- package/Tests/Types/AI/AIRunStatus.test.ts +42 -0
- package/Tests/Types/Log/LogSeverity.test.ts +78 -0
- package/Tests/UI/Components/BasicFormDropdownNormalization.test.tsx +167 -0
- package/Tests/UI/Components/Charts/AxisTickColor.test.tsx +66 -0
- package/Tests/UI/Utils/AIChatExport/ChatWidgetData.test.ts +319 -0
- package/Tests/UI/Utils/AIChatExport/ChatWidgetMarkdown.test.ts +188 -0
- package/Types/AI/AIAgentTaskStatus.ts +13 -0
- package/Types/AI/AIChatPageContext.ts +137 -0
- package/Types/AI/AIChatTypes.ts +44 -0
- package/Types/AI/AIFixReadiness.ts +46 -0
- package/Types/AI/AIRunStatus.ts +11 -0
- package/Types/Email/EmailTemplateType.ts +2 -0
- package/Types/Log/LogSeverity.ts +61 -0
- package/Types/Monitor/SnmpMonitor/SnmpVersion.ts +40 -0
- package/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.tsx +8 -2
- package/UI/Components/Charts/ChartLibrary/BarChart/BarChart.tsx +3 -1
- package/UI/Components/Charts/ChartLibrary/LineChart/LineChart.tsx +8 -2
- package/UI/Components/EditionLabel/EditionLabel.tsx +821 -331
- package/UI/Components/Forms/BasicForm.tsx +26 -8
- package/UI/Styles/Theme.css +1314 -0
- package/UI/Utils/AIChatExport/ChatWidgetData.ts +316 -0
- package/UI/Utils/AIChatExport/ChatWidgetMarkdown.ts +349 -0
- package/UI/Utils/AIChatExport/ConversationMarkdown.ts +177 -0
- package/UI/Utils/AIChatExport/MarkdownBlocks.ts +327 -0
- package/UI/Utils/AIChatExport/MarkdownSafety.ts +215 -0
- package/UI/Utils/DownloadFile.ts +54 -0
- package/build/dist/Models/DatabaseModels/AIAgentTaskPullRequest.js +15 -7
- package/build/dist/Models/DatabaseModels/AIAgentTaskPullRequest.js.map +1 -1
- package/build/dist/Models/DatabaseModels/AIRun.js +69 -0
- package/build/dist/Models/DatabaseModels/AIRun.js.map +1 -1
- package/build/dist/Models/DatabaseModels/AIRunEvent.js +31 -0
- package/build/dist/Models/DatabaseModels/AIRunEvent.js.map +1 -1
- package/build/dist/Models/DatabaseModels/NetworkDeviceDiscoveryScan.js +225 -0
- package/build/dist/Models/DatabaseModels/NetworkDeviceDiscoveryScan.js.map +1 -1
- package/build/dist/Models/DatabaseModels/Project.js +27 -0
- package/build/dist/Models/DatabaseModels/Project.js.map +1 -1
- package/build/dist/Models/DatabaseModels/StatusPage.js +43 -0
- package/build/dist/Models/DatabaseModels/StatusPage.js.map +1 -1
- package/build/dist/Server/API/AIAgentTaskAPI.js +37 -11
- package/build/dist/Server/API/AIAgentTaskAPI.js.map +1 -1
- package/build/dist/Server/API/AIAgentTaskLogAPI.js +32 -7
- package/build/dist/Server/API/AIAgentTaskLogAPI.js.map +1 -1
- package/build/dist/Server/API/AIChatAPI.js +9 -0
- package/build/dist/Server/API/AIChatAPI.js.map +1 -1
- package/build/dist/Server/API/AIReadinessAPI.js +57 -0
- package/build/dist/Server/API/AIReadinessAPI.js.map +1 -0
- package/build/dist/Server/API/CodeFixRunAPI.js +165 -51
- package/build/dist/Server/API/CodeFixRunAPI.js.map +1 -1
- package/build/dist/Server/API/TelemetryExceptionAPI.js +4 -0
- package/build/dist/Server/API/TelemetryExceptionAPI.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1784105912819-BackfillCodeFixTaskType.js +36 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1784105912819-BackfillCodeFixTaskType.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1784124919694-AddAITaskNumberAndRunTranscript.js +80 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1784124919694-AddAITaskNumberAndRunTranscript.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1784135099754-AllowNullAiAgentOnPullRequest.js +37 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1784135099754-AllowNullAiAgentOnPullRequest.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1784137457184-AddEnableMcpServerToStatusPage.js +23 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1784137457184-AddEnableMcpServerToStatusPage.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1784200000000-AddSnmpV3ColumnsToNetworkDeviceDiscoveryScan.js +22 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1784200000000-AddSnmpV3ColumnsToNetworkDeviceDiscoveryScan.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +10 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
- package/build/dist/Server/Middleware/MasterAdminAuthorization.js +1 -1
- package/build/dist/Server/Middleware/ProjectAuthorization.js +1 -1
- package/build/dist/Server/Services/AIRunEventService.js +8 -0
- package/build/dist/Server/Services/AIRunEventService.js.map +1 -1
- package/build/dist/Server/Services/AIRunService.js +39 -3
- package/build/dist/Server/Services/AIRunService.js.map +1 -1
- package/build/dist/Server/Services/LlmProviderService.js +2 -0
- package/build/dist/Server/Services/LlmProviderService.js.map +1 -1
- package/build/dist/Server/Services/ProjectService.js +39 -0
- package/build/dist/Server/Services/ProjectService.js.map +1 -1
- package/build/dist/Server/Services/StatusPageService.js +81 -0
- package/build/dist/Server/Services/StatusPageService.js.map +1 -1
- package/build/dist/Server/Services/TelemetryExceptionService.js +14 -61
- package/build/dist/Server/Services/TelemetryExceptionService.js.map +1 -1
- package/build/dist/Server/Types/Markdown.js +8 -8
- package/build/dist/Server/Types/Markdown.js.map +1 -1
- package/build/dist/Server/Types/MarkdownSlugify.js +47 -0
- package/build/dist/Server/Types/MarkdownSlugify.js.map +1 -0
- package/build/dist/Server/Utils/AI/AIRunPrivacyFilter.js +82 -0
- package/build/dist/Server/Utils/AI/AIRunPrivacyFilter.js.map +1 -0
- package/build/dist/Server/Utils/AI/AIRunTranscript.js +180 -0
- package/build/dist/Server/Utils/AI/AIRunTranscript.js.map +1 -0
- package/build/dist/Server/Utils/AI/Chat/ChatAgentRunner.js +1 -0
- package/build/dist/Server/Utils/AI/Chat/ChatAgentRunner.js.map +1 -1
- package/build/dist/Server/Utils/AI/Chat/ObservabilityChatPrompt.js +103 -5
- package/build/dist/Server/Utils/AI/Chat/ObservabilityChatPrompt.js.map +1 -1
- package/build/dist/Server/Utils/AI/CodeFix/CodeFixAgentCompletion.js +55 -0
- package/build/dist/Server/Utils/AI/CodeFix/CodeFixAgentCompletion.js.map +1 -1
- package/build/dist/Server/Utils/AI/CodeFix/CodeFixReadiness.js +211 -0
- package/build/dist/Server/Utils/AI/CodeFix/CodeFixReadiness.js.map +1 -0
- package/build/dist/Server/Utils/AI/SRE/Insights/FixRouting.js.map +1 -1
- package/build/dist/Server/Utils/AI/Toolbox/CodeTools.js +654 -0
- package/build/dist/Server/Utils/AI/Toolbox/CodeTools.js.map +1 -0
- package/build/dist/Server/Utils/AI/Toolbox/CodeWriteTools.js +389 -0
- package/build/dist/Server/Utils/AI/Toolbox/CodeWriteTools.js.map +1 -0
- package/build/dist/Server/Utils/AI/Toolbox/Index.js +19 -0
- package/build/dist/Server/Utils/AI/Toolbox/Index.js.map +1 -1
- package/build/dist/Server/Utils/AI/Toolbox/ScheduledMaintenanceTools.js +239 -0
- package/build/dist/Server/Utils/AI/Toolbox/ScheduledMaintenanceTools.js.map +1 -0
- package/build/dist/Server/Utils/AnalyticsDatabase/ClickhouseCapacity.js +19 -1
- package/build/dist/Server/Utils/AnalyticsDatabase/ClickhouseCapacity.js.map +1 -1
- package/build/dist/Server/Utils/CodeRepository/GitHub/GitHub.js +439 -0
- package/build/dist/Server/Utils/CodeRepository/GitHub/GitHub.js.map +1 -1
- package/build/dist/Server/Utils/CodeRepository/StackTraceRepoResolver.js +1 -1
- package/build/dist/Server/Utils/CodeRepository/StackTraceRepoResolver.js.map +1 -1
- package/build/dist/Server/Utils/LLM/LLMService.js +275 -19
- package/build/dist/Server/Utils/LLM/LLMService.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/NetworkDeviceHydrationUtil.js +2 -11
- package/build/dist/Server/Utils/Monitor/NetworkDeviceHydrationUtil.js.map +1 -1
- package/build/dist/Types/AI/AIAgentTaskStatus.js +12 -0
- package/build/dist/Types/AI/AIAgentTaskStatus.js.map +1 -1
- package/build/dist/Types/AI/AIChatPageContext.js +101 -0
- package/build/dist/Types/AI/AIChatPageContext.js.map +1 -0
- package/build/dist/Types/AI/AIChatTypes.js +2 -0
- package/build/dist/Types/AI/AIChatTypes.js.map +1 -1
- package/build/dist/Types/AI/AIFixReadiness.js +9 -0
- package/build/dist/Types/AI/AIFixReadiness.js.map +1 -0
- package/build/dist/Types/AI/AIRunStatus.js +11 -0
- package/build/dist/Types/AI/AIRunStatus.js.map +1 -1
- package/build/dist/Types/Email/EmailTemplateType.js +1 -0
- package/build/dist/Types/Email/EmailTemplateType.js.map +1 -1
- package/build/dist/Types/Log/LogSeverity.js +56 -0
- package/build/dist/Types/Log/LogSeverity.js.map +1 -1
- package/build/dist/Types/Monitor/SnmpMonitor/SnmpVersion.js +38 -0
- package/build/dist/Types/Monitor/SnmpMonitor/SnmpVersion.js.map +1 -1
- package/build/dist/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.js +9 -2
- package/build/dist/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.js.map +1 -1
- package/build/dist/UI/Components/Charts/ChartLibrary/BarChart/BarChart.js +3 -1
- package/build/dist/UI/Components/Charts/ChartLibrary/BarChart/BarChart.js.map +1 -1
- package/build/dist/UI/Components/Charts/ChartLibrary/LineChart/LineChart.js +9 -2
- package/build/dist/UI/Components/Charts/ChartLibrary/LineChart/LineChart.js.map +1 -1
- package/build/dist/UI/Components/EditionLabel/EditionLabel.js +424 -139
- package/build/dist/UI/Components/EditionLabel/EditionLabel.js.map +1 -1
- package/build/dist/UI/Components/Forms/BasicForm.js +24 -6
- package/build/dist/UI/Components/Forms/BasicForm.js.map +1 -1
- package/build/dist/UI/Utils/AIChatExport/ChatWidgetData.js +217 -0
- package/build/dist/UI/Utils/AIChatExport/ChatWidgetData.js.map +1 -0
- package/build/dist/UI/Utils/AIChatExport/ChatWidgetMarkdown.js +234 -0
- package/build/dist/UI/Utils/AIChatExport/ChatWidgetMarkdown.js.map +1 -0
- package/build/dist/UI/Utils/AIChatExport/ConversationMarkdown.js +111 -0
- package/build/dist/UI/Utils/AIChatExport/ConversationMarkdown.js.map +1 -0
- package/build/dist/UI/Utils/AIChatExport/MarkdownBlocks.js +233 -0
- package/build/dist/UI/Utils/AIChatExport/MarkdownBlocks.js.map +1 -0
- package/build/dist/UI/Utils/AIChatExport/MarkdownSafety.js +134 -0
- package/build/dist/UI/Utils/AIChatExport/MarkdownSafety.js.map +1 -0
- package/build/dist/UI/Utils/DownloadFile.js +41 -0
- package/build/dist/UI/Utils/DownloadFile.js.map +1 -0
- package/package.json +5 -1
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import BasicForm from "../../../UI/Components/Forms/BasicForm";
|
|
2
|
+
import Fields from "../../../UI/Components/Forms/Types/Fields";
|
|
3
|
+
import FormFieldSchemaType from "../../../UI/Components/Forms/Types/FormFieldSchemaType";
|
|
4
|
+
import FormValues from "../../../UI/Components/Forms/Types/FormValues";
|
|
5
|
+
import { DropdownOption } from "../../../UI/Components/Dropdown/Dropdown";
|
|
6
|
+
import ObjectID from "../../../Types/ObjectID";
|
|
7
|
+
import { render, screen } from "@testing-library/react";
|
|
8
|
+
import userEvent from "@testing-library/user-event";
|
|
9
|
+
import * as React from "react";
|
|
10
|
+
import { describe, expect, test } from "@jest/globals";
|
|
11
|
+
import getJestMockFunction, { MockFunction } from "../../MockType";
|
|
12
|
+
|
|
13
|
+
/*
|
|
14
|
+
* BasicForm's initial-values effect canonicalizes each Dropdown field's stored
|
|
15
|
+
* value onto the matching option. It used to finish with
|
|
16
|
+
*
|
|
17
|
+
* values[fieldName] = dropdownOption?.value || null;
|
|
18
|
+
*
|
|
19
|
+
* which silently replaced any value that matched no option with null. That is
|
|
20
|
+
* data loss, not cosmetics: ModelForm submits every selected field, so an
|
|
21
|
+
* untouched field whose value was wiped here is PUT back as null and clears
|
|
22
|
+
* the column.
|
|
23
|
+
*
|
|
24
|
+
* Options legitimately fail to contain a valid stored value — a
|
|
25
|
+
* permission-scoped or truncated entity list, an options fetch that failed, or
|
|
26
|
+
* a free-text column holding a spelling the options don't list verbatim
|
|
27
|
+
* (NetworkDevice.snmpVersion stores either "V3" or the raw enum "3", and the
|
|
28
|
+
* SNMP form's showIf tolerates both via SnmpVersionUtil — but only if the
|
|
29
|
+
* value survives long enough to be read).
|
|
30
|
+
*
|
|
31
|
+
* These tests drive the real component and assert on the values it actually
|
|
32
|
+
* submits, because the normalization lives in a useEffect and re-implementing
|
|
33
|
+
* it in a test would pin nothing.
|
|
34
|
+
*
|
|
35
|
+
* Note: jest-dom matchers (toBeInTheDocument etc.) fail typecheck repo-wide,
|
|
36
|
+
* so assertions here stay on submitted values and plain DOM text.
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
describe("BasicForm dropdown initial-value normalization", () => {
|
|
40
|
+
const OPTIONS: Array<DropdownOption> = [
|
|
41
|
+
{ label: "V1", value: "V1" },
|
|
42
|
+
{ label: "V2c", value: "V2c" },
|
|
43
|
+
{ label: "V3", value: "V3" },
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
type BuildFieldsFunction = (
|
|
47
|
+
options: Array<DropdownOption>,
|
|
48
|
+
) => Fields<FormValues<any>>;
|
|
49
|
+
|
|
50
|
+
const buildFields: BuildFieldsFunction = (
|
|
51
|
+
options: Array<DropdownOption>,
|
|
52
|
+
): Fields<FormValues<any>> => {
|
|
53
|
+
return [
|
|
54
|
+
{
|
|
55
|
+
field: { snmpVersion: true },
|
|
56
|
+
title: "SNMP Version",
|
|
57
|
+
fieldType: FormFieldSchemaType.Dropdown,
|
|
58
|
+
dropdownOptions: options,
|
|
59
|
+
required: false,
|
|
60
|
+
},
|
|
61
|
+
];
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
type SubmitFormFunction = (
|
|
65
|
+
initialValues: FormValues<any>,
|
|
66
|
+
fields: Fields<FormValues<any>>,
|
|
67
|
+
) => Promise<FormValues<any>>;
|
|
68
|
+
|
|
69
|
+
/*
|
|
70
|
+
* Renders the form, clicks Save, and hands back the values BasicForm
|
|
71
|
+
* submitted — i.e. what would be written to the database.
|
|
72
|
+
*/
|
|
73
|
+
const submitForm: SubmitFormFunction = async (
|
|
74
|
+
initialValues: FormValues<any>,
|
|
75
|
+
fields: Fields<FormValues<any>>,
|
|
76
|
+
): Promise<FormValues<any>> => {
|
|
77
|
+
const onSubmit: MockFunction = getJestMockFunction();
|
|
78
|
+
|
|
79
|
+
render(
|
|
80
|
+
<BasicForm
|
|
81
|
+
fields={fields}
|
|
82
|
+
id="dropdown-normalization"
|
|
83
|
+
initialValues={initialValues}
|
|
84
|
+
onSubmit={onSubmit}
|
|
85
|
+
submitButtonText="Save"
|
|
86
|
+
/>,
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
await userEvent.setup().click(screen.getByTestId("Save"));
|
|
90
|
+
|
|
91
|
+
return onSubmit.mock.calls[0]?.[0] as FormValues<any>;
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
test("keeps a stored value that matches no option instead of nulling it", async () => {
|
|
95
|
+
/*
|
|
96
|
+
* The reported case: a device whose snmpVersion column holds the raw enum
|
|
97
|
+
* spelling "3" while the dropdown lists "V1"/"V2c"/"V3".
|
|
98
|
+
*/
|
|
99
|
+
const submitted: FormValues<any> = await submitForm(
|
|
100
|
+
{ snmpVersion: "3" } as FormValues<any>,
|
|
101
|
+
buildFields(OPTIONS),
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
expect(submitted).toEqual({ snmpVersion: "3" });
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test("keeps stored values when the options failed to load", async () => {
|
|
108
|
+
/*
|
|
109
|
+
* ModelForm sets dropdownOptions to [] when an entity list comes back
|
|
110
|
+
* empty or its fetch throws. Nulling on no-match wiped every dropdown on
|
|
111
|
+
* such a form.
|
|
112
|
+
*/
|
|
113
|
+
const submitted: FormValues<any> = await submitForm(
|
|
114
|
+
{ snmpVersion: "V3" } as FormValues<any>,
|
|
115
|
+
buildFields([]),
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
expect(submitted).toEqual({ snmpVersion: "V3" });
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test("still canonicalizes a value that does match an option", async () => {
|
|
122
|
+
const submitted: FormValues<any> = await submitForm(
|
|
123
|
+
{ snmpVersion: "V3" } as FormValues<any>,
|
|
124
|
+
buildFields(OPTIONS),
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
expect(submitted).toEqual({ snmpVersion: "V3" });
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test("converts a matching ObjectID initial value to its string form", async () => {
|
|
131
|
+
const id: ObjectID = new ObjectID("3f1b6b0e-0000-4000-8000-000000000001");
|
|
132
|
+
|
|
133
|
+
const submitted: FormValues<any> = await submitForm(
|
|
134
|
+
{ snmpVersion: id } as unknown as FormValues<any>,
|
|
135
|
+
buildFields([{ label: "Probe A", value: id.toString() }]),
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
expect(submitted).toEqual({ snmpVersion: id.toString() });
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
test("preserves an unmatched ObjectID as a string, not as an ObjectID", async () => {
|
|
142
|
+
/*
|
|
143
|
+
* An entity dropdown whose referenced row is missing from the (possibly
|
|
144
|
+
* permission-scoped) option list must still submit the canonical string
|
|
145
|
+
* the API expects — preserving the raw ObjectID instance would send an
|
|
146
|
+
* object.
|
|
147
|
+
*/
|
|
148
|
+
const id: ObjectID = new ObjectID("3f1b6b0e-0000-4000-8000-000000000002");
|
|
149
|
+
|
|
150
|
+
const submitted: FormValues<any> = await submitForm(
|
|
151
|
+
{ snmpVersion: id } as unknown as FormValues<any>,
|
|
152
|
+
buildFields([{ label: "Someone else", value: "a-different-id" }]),
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
expect(submitted).toEqual({ snmpVersion: id.toString() });
|
|
156
|
+
expect(typeof submitted["snmpVersion"]).toBe("string");
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test("leaves an absent value alone", async () => {
|
|
160
|
+
const submitted: FormValues<any> = await submitForm(
|
|
161
|
+
{} as FormValues<any>,
|
|
162
|
+
buildFields(OPTIONS),
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
expect(submitted["snmpVersion"]).toBeUndefined();
|
|
166
|
+
});
|
|
167
|
+
});
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { AreaChart } from "../../../../UI/Components/Charts/ChartLibrary/AreaChart/AreaChart";
|
|
2
|
+
import { BarChart } from "../../../../UI/Components/Charts/ChartLibrary/BarChart/BarChart";
|
|
3
|
+
import { LineChart } from "../../../../UI/Components/Charts/ChartLibrary/LineChart/LineChart";
|
|
4
|
+
import "@testing-library/jest-dom/extend-expect";
|
|
5
|
+
import { render } from "@testing-library/react";
|
|
6
|
+
import React from "react";
|
|
7
|
+
import { describe, expect, test } from "@jest/globals";
|
|
8
|
+
|
|
9
|
+
/*
|
|
10
|
+
* ResponsiveContainer measures its parent, which is always 0x0 in jsdom, so the
|
|
11
|
+
* chart renders nothing without a fixed size.
|
|
12
|
+
*/
|
|
13
|
+
jest.mock("recharts", () => {
|
|
14
|
+
const actual: Record<string, any> = jest.requireActual("recharts");
|
|
15
|
+
return {
|
|
16
|
+
...actual,
|
|
17
|
+
ResponsiveContainer: ({ children }: { children: React.ReactElement }) => {
|
|
18
|
+
return React.cloneElement(children, { width: 600, height: 300 });
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
type ChartData = Record<string, any>;
|
|
24
|
+
|
|
25
|
+
const data: ChartData[] = [
|
|
26
|
+
{ time: "10:00", Requests: 1200 },
|
|
27
|
+
{ time: "10:05", Requests: 2400 },
|
|
28
|
+
{ time: "10:10", Requests: 1800 },
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
const CHARTS: Array<[string, React.ReactElement]> = [
|
|
32
|
+
[
|
|
33
|
+
"LineChart",
|
|
34
|
+
<LineChart data={data} index="time" categories={["Requests"]} />,
|
|
35
|
+
],
|
|
36
|
+
[
|
|
37
|
+
"AreaChart",
|
|
38
|
+
<AreaChart data={data} index="time" categories={["Requests"]} />,
|
|
39
|
+
],
|
|
40
|
+
["BarChart", <BarChart data={data} index="time" categories={["Requests"]} />],
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
describe("Chart axis tick colour", () => {
|
|
44
|
+
/*
|
|
45
|
+
* Recharts >= 3.4 portals tick labels into a separate z-index layer, outside the
|
|
46
|
+
* axis <g>. Anything that relies on the axis's class or on fill inheritance to
|
|
47
|
+
* colour them silently paints black, which is invisible in dark mode. The tick
|
|
48
|
+
* must therefore carry its own fill.
|
|
49
|
+
*/
|
|
50
|
+
test.each(CHARTS)(
|
|
51
|
+
"%s gives axis ticks an explicit themeable fill",
|
|
52
|
+
(_name: string, element: React.ReactElement) => {
|
|
53
|
+
const { container } = render(element);
|
|
54
|
+
|
|
55
|
+
const ticks: Array<Element> = Array.from(
|
|
56
|
+
container.querySelectorAll(".recharts-cartesian-axis-tick-value"),
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
expect(ticks.length).toBeGreaterThan(0);
|
|
60
|
+
|
|
61
|
+
for (const tick of ticks) {
|
|
62
|
+
expect(tick.getAttribute("fill")).toBe("var(--ou-chart-tick)");
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
);
|
|
66
|
+
});
|
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
import { describe, expect, test } from "@jest/globals";
|
|
2
|
+
import {
|
|
3
|
+
AIChatWidget,
|
|
4
|
+
AIChatWidgetSpan,
|
|
5
|
+
AIChatWidgetType,
|
|
6
|
+
} from "../../../../Types/AI/AIChatTypes";
|
|
7
|
+
import {
|
|
8
|
+
ExportChartSeries,
|
|
9
|
+
SpanBarMetrics,
|
|
10
|
+
buildParentBySpanId,
|
|
11
|
+
chartHasData,
|
|
12
|
+
chartIsTimeBased,
|
|
13
|
+
computeSpanBar,
|
|
14
|
+
computeSpanDepth,
|
|
15
|
+
formatChartValue,
|
|
16
|
+
formatStatValue,
|
|
17
|
+
formatTableCell,
|
|
18
|
+
formatToolArguments,
|
|
19
|
+
toEntityListEntry,
|
|
20
|
+
toExportChartSeries,
|
|
21
|
+
totalTraceDuration,
|
|
22
|
+
} from "../../../../UI/Utils/AIChatExport/ChatWidgetData";
|
|
23
|
+
|
|
24
|
+
/*
|
|
25
|
+
* These helpers exist to keep the exports in step with what the chat renders,
|
|
26
|
+
* so the tests pin the behaviors that are easy to "tidy up" by accident — the
|
|
27
|
+
* dash for empty cells, 0 not being empty, and a numeric string deliberately
|
|
28
|
+
* not getting the grouping a real number gets.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
function chartWidget(data: AIChatWidget["data"]): AIChatWidget {
|
|
32
|
+
return {
|
|
33
|
+
id: "W1",
|
|
34
|
+
type: AIChatWidgetType.TimeSeriesChart,
|
|
35
|
+
title: "Chart",
|
|
36
|
+
data: data,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
describe("ChatWidgetData", () => {
|
|
41
|
+
describe("formatTableCell", () => {
|
|
42
|
+
test("renders an em dash for null, undefined and empty string only", () => {
|
|
43
|
+
const column: { key: string; title: string } = { key: "a", title: "A" };
|
|
44
|
+
|
|
45
|
+
expect(formatTableCell(null, column)).toBe("—");
|
|
46
|
+
expect(formatTableCell(undefined, column)).toBe("—");
|
|
47
|
+
expect(formatTableCell("", column)).toBe("—");
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("keeps falsy-but-real values, which are not empty", () => {
|
|
51
|
+
const column: { key: string; title: string } = { key: "a", title: "A" };
|
|
52
|
+
|
|
53
|
+
expect(formatTableCell(0, column)).toBe("0");
|
|
54
|
+
expect(formatTableCell(false, column)).toBe("false");
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test("groups a number column, but only when the value is really a number", () => {
|
|
58
|
+
const numeric: { key: string; title: string; type: "number" } = {
|
|
59
|
+
key: "n",
|
|
60
|
+
title: "N",
|
|
61
|
+
type: "number",
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
expect(formatTableCell(1234567, numeric)).toBe(
|
|
65
|
+
(1234567).toLocaleString(),
|
|
66
|
+
);
|
|
67
|
+
// A numeric string falls through to String() and keeps no separators.
|
|
68
|
+
expect(formatTableCell("1234567", numeric)).toBe("1234567");
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test("stringifies anything else", () => {
|
|
72
|
+
const column: { key: string; title: string } = { key: "a", title: "A" };
|
|
73
|
+
expect(formatTableCell(["a", "b"], column)).toBe("a,b");
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
describe("formatStatValue", () => {
|
|
78
|
+
test("groups numbers and passes strings through verbatim", () => {
|
|
79
|
+
expect(formatStatValue({ label: "x", value: 1284322 })).toBe(
|
|
80
|
+
(1284322).toLocaleString(),
|
|
81
|
+
);
|
|
82
|
+
expect(formatStatValue({ label: "x", value: "1284322" })).toBe("1284322");
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test("appends the unit when there is one", () => {
|
|
86
|
+
expect(formatStatValue({ label: "x", value: 12, unit: "ms" })).toBe(
|
|
87
|
+
"12 ms",
|
|
88
|
+
);
|
|
89
|
+
expect(formatStatValue({ label: "x", value: 12, unit: "" })).toBe("12");
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
describe("formatChartValue", () => {
|
|
94
|
+
test("rounds to a whole number at or above 100, and to 2dp below", () => {
|
|
95
|
+
expect(formatChartValue(1234.56)).toBe((1235).toLocaleString());
|
|
96
|
+
expect(formatChartValue(12.345)).toBe((12.35).toLocaleString());
|
|
97
|
+
expect(formatChartValue(-1234.56)).toBe((-1235).toLocaleString());
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test("appends the unit after a single space", () => {
|
|
101
|
+
expect(formatChartValue(12.5, "ms")).toBe("12.5 ms");
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
describe("formatToolArguments", () => {
|
|
106
|
+
test("skips empty values but keeps 0 and false", () => {
|
|
107
|
+
expect(
|
|
108
|
+
formatToolArguments({ a: null, b: undefined, c: "", d: 0, e: false }),
|
|
109
|
+
).toBe("d: 0 · e: false");
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test("json-encodes objects and arrays", () => {
|
|
113
|
+
expect(formatToolArguments({ tags: ["a", "b"] })).toBe('tags: ["a","b"]');
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test("truncates a long value with an ellipsis, never the key", () => {
|
|
117
|
+
const long: string = "x".repeat(200);
|
|
118
|
+
const result: string = formatToolArguments({ key: long });
|
|
119
|
+
|
|
120
|
+
expect(result.startsWith("key: ")).toBe(true);
|
|
121
|
+
expect(result.endsWith("…")).toBe(true);
|
|
122
|
+
expect(result).toBe(`key: ${"x".repeat(80)}…`);
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
describe("span helpers", () => {
|
|
127
|
+
const spans: Array<AIChatWidgetSpan> = [
|
|
128
|
+
{
|
|
129
|
+
spanId: "a",
|
|
130
|
+
name: "root",
|
|
131
|
+
startOffsetMs: 0,
|
|
132
|
+
durationMs: 100,
|
|
133
|
+
isError: false,
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
spanId: "b",
|
|
137
|
+
parentSpanId: "a",
|
|
138
|
+
name: "child",
|
|
139
|
+
startOffsetMs: 10,
|
|
140
|
+
durationMs: 50,
|
|
141
|
+
isError: false,
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
spanId: "c",
|
|
145
|
+
parentSpanId: "b",
|
|
146
|
+
name: "grandchild",
|
|
147
|
+
startOffsetMs: 20,
|
|
148
|
+
durationMs: 10,
|
|
149
|
+
isError: true,
|
|
150
|
+
},
|
|
151
|
+
];
|
|
152
|
+
|
|
153
|
+
test("depth counts the parent chain", () => {
|
|
154
|
+
const parents: Map<string, string | undefined> =
|
|
155
|
+
buildParentBySpanId(spans);
|
|
156
|
+
|
|
157
|
+
expect(computeSpanDepth(spans[0] as AIChatWidgetSpan, parents)).toBe(0);
|
|
158
|
+
expect(computeSpanDepth(spans[1] as AIChatWidgetSpan, parents)).toBe(1);
|
|
159
|
+
expect(computeSpanDepth(spans[2] as AIChatWidgetSpan, parents)).toBe(2);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
test("a parent cycle terminates instead of hanging", () => {
|
|
163
|
+
const cyclic: Array<AIChatWidgetSpan> = [
|
|
164
|
+
{
|
|
165
|
+
spanId: "x",
|
|
166
|
+
parentSpanId: "y",
|
|
167
|
+
name: "x",
|
|
168
|
+
startOffsetMs: 0,
|
|
169
|
+
durationMs: 1,
|
|
170
|
+
isError: false,
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
spanId: "y",
|
|
174
|
+
parentSpanId: "x",
|
|
175
|
+
name: "y",
|
|
176
|
+
startOffsetMs: 0,
|
|
177
|
+
durationMs: 1,
|
|
178
|
+
isError: false,
|
|
179
|
+
},
|
|
180
|
+
];
|
|
181
|
+
const parents: Map<string, string | undefined> =
|
|
182
|
+
buildParentBySpanId(cyclic);
|
|
183
|
+
|
|
184
|
+
expect(
|
|
185
|
+
computeSpanDepth(cyclic[0] as AIChatWidgetSpan, parents),
|
|
186
|
+
).toBeLessThanOrEqual(13);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
test("the bar keeps a visible minimum width and stays inside the track", () => {
|
|
190
|
+
// A zero-duration span still gets the 1.5% floor.
|
|
191
|
+
const zero: SpanBarMetrics = computeSpanBar(
|
|
192
|
+
{
|
|
193
|
+
spanId: "z",
|
|
194
|
+
name: "z",
|
|
195
|
+
startOffsetMs: 0,
|
|
196
|
+
durationMs: 0,
|
|
197
|
+
isError: false,
|
|
198
|
+
},
|
|
199
|
+
1000,
|
|
200
|
+
);
|
|
201
|
+
expect(zero.widthPercent).toBe(1.5);
|
|
202
|
+
|
|
203
|
+
// A span running past the end is clipped to the right edge.
|
|
204
|
+
const overflowing: SpanBarMetrics = computeSpanBar(
|
|
205
|
+
{
|
|
206
|
+
spanId: "o",
|
|
207
|
+
name: "o",
|
|
208
|
+
startOffsetMs: 900,
|
|
209
|
+
durationMs: 5000,
|
|
210
|
+
isError: false,
|
|
211
|
+
},
|
|
212
|
+
1000,
|
|
213
|
+
);
|
|
214
|
+
expect(overflowing.leftPercent).toBe(90);
|
|
215
|
+
expect(overflowing.widthPercent).toBe(10);
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
test("the denominator never reaches zero", () => {
|
|
219
|
+
expect(
|
|
220
|
+
totalTraceDuration({
|
|
221
|
+
id: "W",
|
|
222
|
+
type: AIChatWidgetType.TraceWaterfall,
|
|
223
|
+
title: "t",
|
|
224
|
+
data: { totalDurationMs: 0 },
|
|
225
|
+
}),
|
|
226
|
+
).toBe(1);
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
describe("toEntityListEntry", () => {
|
|
231
|
+
test("reads the exception fields, and 0 occurrences still shows a count", () => {
|
|
232
|
+
const entry: { heading: string; badges: Array<string> } =
|
|
233
|
+
toEntityListEntry(
|
|
234
|
+
{ type: "TimeoutError", message: "boom", occurrences: 0 },
|
|
235
|
+
AIChatWidgetType.ExceptionList,
|
|
236
|
+
);
|
|
237
|
+
|
|
238
|
+
expect(entry.heading).toBe("TimeoutError");
|
|
239
|
+
expect(entry.badges).toContain("0×");
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
test("falls back to a generic exception heading", () => {
|
|
243
|
+
const entry: { heading: string } = toEntityListEntry(
|
|
244
|
+
{ message: "boom" },
|
|
245
|
+
AIChatWidgetType.ExceptionList,
|
|
246
|
+
);
|
|
247
|
+
expect(entry.heading).toBe("Exception");
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
test("prefixes an incident with its number", () => {
|
|
251
|
+
const entry: { heading: string; badges: Array<string> } =
|
|
252
|
+
toEntityListEntry(
|
|
253
|
+
{
|
|
254
|
+
incidentNumber: 412,
|
|
255
|
+
title: "Down",
|
|
256
|
+
state: "Resolved",
|
|
257
|
+
severity: "Major",
|
|
258
|
+
},
|
|
259
|
+
AIChatWidgetType.IncidentList,
|
|
260
|
+
);
|
|
261
|
+
|
|
262
|
+
expect(entry.heading).toBe("#412 · Down");
|
|
263
|
+
expect(entry.badges).toEqual(["Resolved", "Major"]);
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
test("an alert reads its own number field", () => {
|
|
267
|
+
const entry: { heading: string } = toEntityListEntry(
|
|
268
|
+
{ alertNumber: 9, incidentNumber: 1, title: "Alert" },
|
|
269
|
+
AIChatWidgetType.AlertList,
|
|
270
|
+
);
|
|
271
|
+
expect(entry.heading).toBe("#9 · Alert");
|
|
272
|
+
});
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
describe("toExportChartSeries", () => {
|
|
276
|
+
test("drops null and undefined y values", () => {
|
|
277
|
+
const series: Array<ExportChartSeries> = toExportChartSeries(
|
|
278
|
+
chartWidget({
|
|
279
|
+
series: [
|
|
280
|
+
{
|
|
281
|
+
name: "s",
|
|
282
|
+
points: [
|
|
283
|
+
{ x: "2026-07-15T10:00:00Z", y: 1 },
|
|
284
|
+
{ x: "2026-07-15T11:00:00Z", y: null },
|
|
285
|
+
],
|
|
286
|
+
},
|
|
287
|
+
],
|
|
288
|
+
}),
|
|
289
|
+
);
|
|
290
|
+
|
|
291
|
+
expect(series[0]?.points).toHaveLength(1);
|
|
292
|
+
expect(series[0]?.points[0]?.y).toBe(1);
|
|
293
|
+
expect(series[0]?.points[0]?.date).toBeInstanceOf(Date);
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
test("reports no data when every series is empty", () => {
|
|
297
|
+
expect(chartHasData(toExportChartSeries(chartWidget({})))).toBe(false);
|
|
298
|
+
expect(
|
|
299
|
+
chartHasData(
|
|
300
|
+
toExportChartSeries(
|
|
301
|
+
chartWidget({ series: [{ name: "s", points: [] }] }),
|
|
302
|
+
),
|
|
303
|
+
),
|
|
304
|
+
).toBe(false);
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
test("a category x value is kept but not treated as a time", () => {
|
|
308
|
+
const series: Array<ExportChartSeries> = toExportChartSeries(
|
|
309
|
+
chartWidget({
|
|
310
|
+
series: [{ name: "s", points: [{ x: "ERROR", y: 2 }] }],
|
|
311
|
+
}),
|
|
312
|
+
);
|
|
313
|
+
|
|
314
|
+
expect(series[0]?.points[0]?.x).toBe("ERROR");
|
|
315
|
+
expect(series[0]?.points[0]?.date).toBeUndefined();
|
|
316
|
+
expect(chartIsTimeBased(series)).toBe(false);
|
|
317
|
+
});
|
|
318
|
+
});
|
|
319
|
+
});
|