@oneuptime/common 11.5.8 → 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 +37 -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 +182 -0
- package/Server/API/TelemetryExceptionAPI.ts +6 -1
- 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 +8 -0
- package/Server/Services/AIRunEventService.ts +15 -1
- package/Server/Services/AIRunService.ts +38 -1
- 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/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/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/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/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 +38 -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 +142 -0
- 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/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 +8 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +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 +29 -0
- 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/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/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/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,223 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* StatusPageService MCP gate tests.
|
|
3
|
+
*
|
|
4
|
+
* resolveStatusPageIdOrNull and isMcpServerEnabled back the unauthenticated
|
|
5
|
+
* public status page MCP tools, so their contract is security-relevant:
|
|
6
|
+
* - a status page that does not exist and one that has MCP turned off must be
|
|
7
|
+
* indistinguishable to the caller (otherwise the tools become a status page
|
|
8
|
+
* enumeration oracle)
|
|
9
|
+
* - a database failure must propagate rather than collapse into "disabled", so
|
|
10
|
+
* an outage is reported as an error instead of being silently reported as an
|
|
11
|
+
* owner's privacy choice
|
|
12
|
+
*
|
|
13
|
+
* Both services' findOneBy are spied on, so no database is touched.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import StatusPageService from "../../../Server/Services/StatusPageService";
|
|
17
|
+
import StatusPageDomainService from "../../../Server/Services/StatusPageDomainService";
|
|
18
|
+
import StatusPage from "../../../Models/DatabaseModels/StatusPage";
|
|
19
|
+
import StatusPageDomain from "../../../Models/DatabaseModels/StatusPageDomain";
|
|
20
|
+
import ObjectID from "../../../Types/ObjectID";
|
|
21
|
+
import { afterEach, describe, expect, test, jest } from "@jest/globals";
|
|
22
|
+
|
|
23
|
+
const VALID_UUID: string = "550e8400-e29b-41d4-a716-446655440000";
|
|
24
|
+
const DOMAIN: string = "status.company.com";
|
|
25
|
+
|
|
26
|
+
function statusPageDomainWith(statusPageId: ObjectID | null): StatusPageDomain {
|
|
27
|
+
return {
|
|
28
|
+
statusPageId: statusPageId ?? undefined,
|
|
29
|
+
} as unknown as StatusPageDomain;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function statusPageRow(): StatusPage {
|
|
33
|
+
return { _id: VALID_UUID } as unknown as StatusPage;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function spyOnStatusPageFindOneBy(): jest.SpyInstance {
|
|
37
|
+
return jest.spyOn(
|
|
38
|
+
StatusPageService,
|
|
39
|
+
"findOneBy",
|
|
40
|
+
) as unknown as jest.SpyInstance;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function spyOnDomainFindOneBy(): jest.SpyInstance {
|
|
44
|
+
return jest.spyOn(
|
|
45
|
+
StatusPageDomainService,
|
|
46
|
+
"findOneBy",
|
|
47
|
+
) as unknown as jest.SpyInstance;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
describe("StatusPageService MCP gate", () => {
|
|
51
|
+
afterEach(() => {
|
|
52
|
+
jest.restoreAllMocks();
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
describe("resolveStatusPageIdOrNull", () => {
|
|
56
|
+
test("resolves a UUID without querying the domain table", async () => {
|
|
57
|
+
const domainSpy: jest.SpyInstance = spyOnDomainFindOneBy();
|
|
58
|
+
|
|
59
|
+
const result: ObjectID | null =
|
|
60
|
+
await StatusPageService.resolveStatusPageIdOrNull(VALID_UUID);
|
|
61
|
+
|
|
62
|
+
expect(result?.toString()).toBe(VALID_UUID);
|
|
63
|
+
expect(domainSpy).not.toHaveBeenCalled();
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test("looks a domain up by fullDomain, requiring a verified domain", async () => {
|
|
67
|
+
const statusPageId: ObjectID = new ObjectID(VALID_UUID);
|
|
68
|
+
const domainSpy: jest.SpyInstance =
|
|
69
|
+
spyOnDomainFindOneBy().mockResolvedValue(
|
|
70
|
+
statusPageDomainWith(statusPageId) as never,
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
const result: ObjectID | null =
|
|
74
|
+
await StatusPageService.resolveStatusPageIdOrNull(DOMAIN);
|
|
75
|
+
|
|
76
|
+
expect(result?.toString()).toBe(VALID_UUID);
|
|
77
|
+
|
|
78
|
+
const args: Record<string, any> = domainSpy.mock.calls[0]![0] as Record<
|
|
79
|
+
string,
|
|
80
|
+
any
|
|
81
|
+
>;
|
|
82
|
+
|
|
83
|
+
expect(args["query"]["fullDomain"]).toBe(DOMAIN);
|
|
84
|
+
expect(args["query"]["domain"]).toEqual({ isVerified: true });
|
|
85
|
+
expect(args["props"]["isRoot"]).toBe(true);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test("returns null when the domain is not registered", async () => {
|
|
89
|
+
spyOnDomainFindOneBy().mockResolvedValue(null as never);
|
|
90
|
+
|
|
91
|
+
await expect(
|
|
92
|
+
StatusPageService.resolveStatusPageIdOrNull(DOMAIN),
|
|
93
|
+
).resolves.toBeNull();
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test("returns null when the domain row carries no status page", async () => {
|
|
97
|
+
spyOnDomainFindOneBy().mockResolvedValue(
|
|
98
|
+
statusPageDomainWith(null) as never,
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
await expect(
|
|
102
|
+
StatusPageService.resolveStatusPageIdOrNull(DOMAIN),
|
|
103
|
+
).resolves.toBeNull();
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test("returns null for an empty identifier without querying", async () => {
|
|
107
|
+
const domainSpy: jest.SpyInstance = spyOnDomainFindOneBy();
|
|
108
|
+
|
|
109
|
+
await expect(
|
|
110
|
+
StatusPageService.resolveStatusPageIdOrNull(""),
|
|
111
|
+
).resolves.toBeNull();
|
|
112
|
+
expect(domainSpy).not.toHaveBeenCalled();
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test("returns null for a malformed UUID rather than throwing", async () => {
|
|
116
|
+
await expect(
|
|
117
|
+
StatusPageService.resolveStatusPageIdOrNull("not-a-uuid"),
|
|
118
|
+
).resolves.toBeNull();
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test("returns null for a dotless hostname", async () => {
|
|
122
|
+
/*
|
|
123
|
+
* The dot is the discriminator, mirroring resolveStatusPageIdOrThrow in
|
|
124
|
+
* StatusPageAPI. "localhost" therefore goes down the UUID path and fails
|
|
125
|
+
* there. Pinned so the two resolvers cannot silently diverge.
|
|
126
|
+
*/
|
|
127
|
+
await expect(
|
|
128
|
+
StatusPageService.resolveStatusPageIdOrNull("localhost"),
|
|
129
|
+
).resolves.toBeNull();
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
describe("isMcpServerEnabled", () => {
|
|
134
|
+
test("is true when a UUID matches a status page with MCP enabled", async () => {
|
|
135
|
+
spyOnStatusPageFindOneBy().mockResolvedValue(statusPageRow() as never);
|
|
136
|
+
|
|
137
|
+
await expect(
|
|
138
|
+
StatusPageService.isMcpServerEnabled(VALID_UUID),
|
|
139
|
+
).resolves.toBe(true);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test("is false when the status page has MCP disabled", async () => {
|
|
143
|
+
/*
|
|
144
|
+
* The enableMcpServer predicate is in the query, so a gated page simply
|
|
145
|
+
* does not match.
|
|
146
|
+
*/
|
|
147
|
+
spyOnStatusPageFindOneBy().mockResolvedValue(null as never);
|
|
148
|
+
|
|
149
|
+
await expect(
|
|
150
|
+
StatusPageService.isMcpServerEnabled(VALID_UUID),
|
|
151
|
+
).resolves.toBe(false);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
test("folds the flag into the query and reads as root", async () => {
|
|
155
|
+
const spy: jest.SpyInstance =
|
|
156
|
+
spyOnStatusPageFindOneBy().mockResolvedValue(statusPageRow() as never);
|
|
157
|
+
|
|
158
|
+
await StatusPageService.isMcpServerEnabled(VALID_UUID);
|
|
159
|
+
|
|
160
|
+
const args: Record<string, any> = spy.mock.calls[0]![0] as Record<
|
|
161
|
+
string,
|
|
162
|
+
any
|
|
163
|
+
>;
|
|
164
|
+
|
|
165
|
+
expect(args["query"]["enableMcpServer"]).toBe(true);
|
|
166
|
+
expect(args["query"]["_id"].toString()).toBe(VALID_UUID);
|
|
167
|
+
expect(args["props"]["isRoot"]).toBe(true);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
test("resolves a domain and then checks the flag", async () => {
|
|
171
|
+
spyOnDomainFindOneBy().mockResolvedValue(
|
|
172
|
+
statusPageDomainWith(new ObjectID(VALID_UUID)) as never,
|
|
173
|
+
);
|
|
174
|
+
const pageSpy: jest.SpyInstance =
|
|
175
|
+
spyOnStatusPageFindOneBy().mockResolvedValue(statusPageRow() as never);
|
|
176
|
+
|
|
177
|
+
await expect(StatusPageService.isMcpServerEnabled(DOMAIN)).resolves.toBe(
|
|
178
|
+
true,
|
|
179
|
+
);
|
|
180
|
+
expect(pageSpy).toHaveBeenCalledTimes(1);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
test("is false for an unknown status page, without a second query", async () => {
|
|
184
|
+
spyOnDomainFindOneBy().mockResolvedValue(null as never);
|
|
185
|
+
const pageSpy: jest.SpyInstance = spyOnStatusPageFindOneBy();
|
|
186
|
+
|
|
187
|
+
await expect(StatusPageService.isMcpServerEnabled(DOMAIN)).resolves.toBe(
|
|
188
|
+
false,
|
|
189
|
+
);
|
|
190
|
+
expect(pageSpy).not.toHaveBeenCalled();
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
test("is false for an unresolvable identifier", async () => {
|
|
194
|
+
await expect(
|
|
195
|
+
StatusPageService.isMcpServerEnabled("not-a-uuid"),
|
|
196
|
+
).resolves.toBe(false);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
test("propagates a status page query failure instead of reporting disabled", async () => {
|
|
200
|
+
/*
|
|
201
|
+
* Fails closed either way, but the caller reports an error rather than
|
|
202
|
+
* telling the agent the owner turned MCP off.
|
|
203
|
+
*/
|
|
204
|
+
spyOnStatusPageFindOneBy().mockRejectedValue(
|
|
205
|
+
new Error("connection terminated") as never,
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
await expect(
|
|
209
|
+
StatusPageService.isMcpServerEnabled(VALID_UUID),
|
|
210
|
+
).rejects.toThrow("connection terminated");
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
test("propagates a domain query failure", async () => {
|
|
214
|
+
spyOnDomainFindOneBy().mockRejectedValue(
|
|
215
|
+
new Error("connection terminated") as never,
|
|
216
|
+
);
|
|
217
|
+
|
|
218
|
+
await expect(
|
|
219
|
+
StatusPageService.isMcpServerEnabled(DOMAIN),
|
|
220
|
+
).rejects.toThrow("connection terminated");
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
});
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import TelemetryExceptionService
|
|
1
|
+
import TelemetryExceptionService from "../../../Server/Services/TelemetryExceptionService";
|
|
2
|
+
import {
|
|
2
3
|
AIFixReadiness,
|
|
3
4
|
AIFixReadinessCheck,
|
|
4
|
-
} from "../../../
|
|
5
|
+
} from "../../../Types/AI/AIFixReadiness";
|
|
5
6
|
import FixRunBudget from "../../../Server/Utils/AI/CodeFix/FixRunBudget";
|
|
6
7
|
import LlmProviderService from "../../../Server/Services/LlmProviderService";
|
|
7
8
|
import ProjectService from "../../../Server/Services/ProjectService";
|
|
@@ -19,7 +20,8 @@ import AIAgent, {
|
|
|
19
20
|
import BadDataException from "../../../Types/Exception/BadDataException";
|
|
20
21
|
import ObjectID from "../../../Types/ObjectID";
|
|
21
22
|
import OneUptimeDate from "../../../Types/Date";
|
|
22
|
-
import
|
|
23
|
+
import AIService from "../../../Server/Services/AIService";
|
|
24
|
+
import { describe, expect, test, afterEach, beforeEach } from "@jest/globals";
|
|
23
25
|
|
|
24
26
|
/*
|
|
25
27
|
* "Fix with AI Agent" readiness: every prerequisite is checked BEFORE a task
|
|
@@ -121,7 +123,24 @@ function getCheck(readiness: AIFixReadiness, id: string): AIFixReadinessCheck {
|
|
|
121
123
|
return check!;
|
|
122
124
|
}
|
|
123
125
|
|
|
126
|
+
/*
|
|
127
|
+
* The llmProvider check also gates on the daily autonomous token budget
|
|
128
|
+
* (AI_CODE_FIX_FEATURE is autonomous, so executeWithLogging enforces it).
|
|
129
|
+
* Default it to unset — these suites are about the other fail conditions.
|
|
130
|
+
*/
|
|
131
|
+
function mockBudgetNotExhausted(): void {
|
|
132
|
+
jest.spyOn(AIService, "getAutonomousDailyBudgetStatus").mockResolvedValue({
|
|
133
|
+
exhausted: false,
|
|
134
|
+
limitInTokens: null,
|
|
135
|
+
usedTokensToday: 0,
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
124
139
|
describe("TelemetryExceptionService.getAIFixReadiness", () => {
|
|
140
|
+
beforeEach(() => {
|
|
141
|
+
mockBudgetNotExhausted();
|
|
142
|
+
});
|
|
143
|
+
|
|
125
144
|
afterEach(() => {
|
|
126
145
|
jest.restoreAllMocks();
|
|
127
146
|
});
|
|
@@ -237,6 +256,10 @@ describe("TelemetryExceptionService.getAIFixReadiness", () => {
|
|
|
237
256
|
* and free-global providers consume no balance and must never require one.
|
|
238
257
|
*/
|
|
239
258
|
describe("TelemetryExceptionService.getAIFixReadiness — AI balance gate", () => {
|
|
259
|
+
beforeEach(() => {
|
|
260
|
+
mockBudgetNotExhausted();
|
|
261
|
+
});
|
|
262
|
+
|
|
240
263
|
afterEach(() => {
|
|
241
264
|
jest.restoreAllMocks();
|
|
242
265
|
});
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import TelemetryExceptionService from "../../../Server/Services/TelemetryExceptionService";
|
|
2
|
+
import AIService from "../../../Server/Services/AIService";
|
|
2
3
|
import FixRunBudget from "../../../Server/Utils/AI/CodeFix/FixRunBudget";
|
|
3
4
|
import LlmProviderService from "../../../Server/Services/LlmProviderService";
|
|
4
5
|
import ServiceService from "../../../Server/Services/ServiceService";
|
|
@@ -79,6 +80,17 @@ function mockReadinessOk(): void {
|
|
|
79
80
|
name: "agent",
|
|
80
81
|
connectionStatus: AIAgentConnectionStatus.Connected,
|
|
81
82
|
} as unknown as AIAgent);
|
|
83
|
+
/*
|
|
84
|
+
* The llmProvider check also gates on the daily autonomous token budget
|
|
85
|
+
* (AI_CODE_FIX_FEATURE is autonomous, so executeWithLogging enforces it).
|
|
86
|
+
* Default it to unset — this suite is about the duplicate guard, and the
|
|
87
|
+
* budget has its own suite (AIServiceDailyBudget.test.ts).
|
|
88
|
+
*/
|
|
89
|
+
jest.spyOn(AIService, "getAutonomousDailyBudgetStatus").mockResolvedValue({
|
|
90
|
+
exhausted: false,
|
|
91
|
+
limitInTokens: null,
|
|
92
|
+
usedTokensToday: 0,
|
|
93
|
+
});
|
|
82
94
|
}
|
|
83
95
|
|
|
84
96
|
describe("TelemetryExceptionService.createCodeFixRunForException", () => {
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import Markdown from "../../../Server/Types/Markdown";
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
* slugify turns a heading into the `id` the docs renderer emits, and therefore
|
|
5
|
+
* into the target every in-page `](#anchor)` link has to match. These tests pin
|
|
6
|
+
* the two properties that are easy to break by accident:
|
|
7
|
+
*
|
|
8
|
+
* - non-ASCII scripts must survive (they used to be stripped to an empty id,
|
|
9
|
+
* which silently 404'd every anchor in the ru/ja/ko/hi/zh docs), and
|
|
10
|
+
* - existing English ids must not move, because they are published URLs.
|
|
11
|
+
*/
|
|
12
|
+
describe("Markdown.slugify", () => {
|
|
13
|
+
describe("English headings keep their existing ids", () => {
|
|
14
|
+
test.each([
|
|
15
|
+
[
|
|
16
|
+
"Reducing the Volume of Data Collected",
|
|
17
|
+
"reducing-the-volume-of-data-collected",
|
|
18
|
+
],
|
|
19
|
+
["Filtering by Log Severity", "filtering-by-log-severity"],
|
|
20
|
+
["Step 1 — Install the Collector", "step-1-install-the-collector"],
|
|
21
|
+
["What's New?", "whats-new"],
|
|
22
|
+
["Namespace Filtering", "namespace-filtering"],
|
|
23
|
+
])("%s", (heading: string, expected: string) => {
|
|
24
|
+
expect(Markdown.slugify(heading)).toBe(expected);
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
/*
|
|
29
|
+
* \p{L}/\p{N} do not match "_", so it is listed explicitly in the character
|
|
30
|
+
* class. The old `\w` matched it. These are real headings — dropping the
|
|
31
|
+
* underscore would change ids that are already linked to.
|
|
32
|
+
*/
|
|
33
|
+
describe("underscores are preserved", () => {
|
|
34
|
+
test.each([
|
|
35
|
+
[
|
|
36
|
+
"`ceph_health_status` is 1 but no incident fires",
|
|
37
|
+
"ceph_health_status-is-1-but-no-incident-fires",
|
|
38
|
+
],
|
|
39
|
+
[
|
|
40
|
+
"Counters like `pve_network_receive_bytes` only ever grow",
|
|
41
|
+
"counters-like-pve_network_receive_bytes-only-ever-grow",
|
|
42
|
+
],
|
|
43
|
+
])("%s", (heading: string, expected: string) => {
|
|
44
|
+
expect(Markdown.slugify(heading)).toBe(expected);
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
describe("non-Latin scripts produce a usable id, not an empty one", () => {
|
|
49
|
+
test.each([
|
|
50
|
+
["Фильтрация по важности логов", "фильтрация-по-важности-логов"],
|
|
51
|
+
["ログの重大度によるフィルタリング", "ログの重大度によるフィルタリング"],
|
|
52
|
+
["按日志严重性过滤", "按日志严重性过滤"],
|
|
53
|
+
["로그 심각도별 필터링", "로그-심각도별-필터링"],
|
|
54
|
+
[
|
|
55
|
+
"लॉग गंभीरता के अनुसार फ़िल्टर करना",
|
|
56
|
+
"लॉग-गंभीरता-के-अनुसार-फ़िल्टर-करना",
|
|
57
|
+
],
|
|
58
|
+
])("%s", (heading: string, expected: string) => {
|
|
59
|
+
const slug: string = Markdown.slugify(heading);
|
|
60
|
+
expect(slug).not.toBe("");
|
|
61
|
+
expect(slug).toBe(expected);
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
describe("accented Latin keeps the accented letter", () => {
|
|
66
|
+
test.each([
|
|
67
|
+
[
|
|
68
|
+
"Filtrage par gravité des journaux",
|
|
69
|
+
"filtrage-par-gravité-des-journaux",
|
|
70
|
+
],
|
|
71
|
+
[
|
|
72
|
+
"Reduktion af mængden af indsamlede data",
|
|
73
|
+
"reduktion-af-mængden-af-indsamlede-data",
|
|
74
|
+
],
|
|
75
|
+
["Fehlerbehebung für Größen", "fehlerbehebung-für-größen"],
|
|
76
|
+
])("%s", (heading: string, expected: string) => {
|
|
77
|
+
expect(Markdown.slugify(heading)).toBe(expected);
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
describe("markup and punctuation are still stripped", () => {
|
|
82
|
+
test("strips inline HTML tags", () => {
|
|
83
|
+
expect(Markdown.slugify("A <em>heading</em> here")).toBe(
|
|
84
|
+
"a-heading-here",
|
|
85
|
+
);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test("strips HTML entities", () => {
|
|
89
|
+
expect(Markdown.slugify("Tom & Jerry")).toBe("tom-jerry");
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test("strips punctuation but keeps hyphens", () => {
|
|
93
|
+
expect(Markdown.slugify("Set up SSO (SAML): step-by-step!")).toBe(
|
|
94
|
+
"set-up-sso-saml-step-by-step",
|
|
95
|
+
);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test("collapses whitespace runs and trims leading/trailing hyphens", () => {
|
|
99
|
+
expect(Markdown.slugify(" spaced out ")).toBe("spaced-out");
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test("emoji are dropped rather than becoming part of the id", () => {
|
|
103
|
+
expect(Markdown.slugify("🚀 Getting Started")).toBe("getting-started");
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
});
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import AIRunTranscript, {
|
|
2
|
+
MAX_MESSAGE_CONTENT_CHARS,
|
|
3
|
+
MAX_PAYLOAD_CHARS,
|
|
4
|
+
MAX_RESPONSE_CONTENT_CHARS,
|
|
5
|
+
MAX_TOOL_ARGUMENT_CHARS,
|
|
6
|
+
MAX_TOOL_RESULT_CHARS,
|
|
7
|
+
} from "../../../../Server/Utils/AI/AIRunTranscript";
|
|
8
|
+
import { AIRunEventContentPayload } from "../../../../Types/AI/AIChatTypes";
|
|
9
|
+
import { describe, expect, test } from "@jest/globals";
|
|
10
|
+
|
|
11
|
+
/*
|
|
12
|
+
* The run transcript is the ONE place a code-fix run's LLM content is stored
|
|
13
|
+
* (LlmLog redacts it — it is project-wide readable and these prompts embed
|
|
14
|
+
* customer source code). It stores content verbatim to be worth reading when
|
|
15
|
+
* a run goes wrong, which makes bounding it a storage requirement rather than
|
|
16
|
+
* a nicety: an agent may read large files and run test suites emitting
|
|
17
|
+
* megabytes, up to 40 times per run.
|
|
18
|
+
*
|
|
19
|
+
* These tests pin the two properties that make that safe: every field is
|
|
20
|
+
* capped, and the payload as a whole cannot exceed MAX_PAYLOAD_CHARS no
|
|
21
|
+
* matter WHICH field carries the bulk.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
type SerializedLengthFunction = (payload: AIRunEventContentPayload) => number;
|
|
25
|
+
|
|
26
|
+
const serializedLength: SerializedLengthFunction = (
|
|
27
|
+
payload: AIRunEventContentPayload,
|
|
28
|
+
): number => {
|
|
29
|
+
return JSON.stringify(payload).length;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
describe("AIRunTranscript.clip", () => {
|
|
33
|
+
test("leaves a value under the cap untouched and unflagged", () => {
|
|
34
|
+
expect(AIRunTranscript.clip("short", 100)).toEqual({
|
|
35
|
+
value: "short",
|
|
36
|
+
isTruncated: false,
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test("clips an oversized value and reports the truncation", () => {
|
|
41
|
+
const result: { value: string | undefined; isTruncated: boolean } =
|
|
42
|
+
AIRunTranscript.clip("x".repeat(50), 10);
|
|
43
|
+
|
|
44
|
+
expect(result.isTruncated).toBe(true);
|
|
45
|
+
expect(result.value).toContain("truncated");
|
|
46
|
+
expect(result.value!.startsWith("x".repeat(10))).toBe(true);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("passes undefined through without inventing a value", () => {
|
|
50
|
+
expect(AIRunTranscript.clip(undefined, 10)).toEqual({
|
|
51
|
+
value: undefined,
|
|
52
|
+
isTruncated: false,
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
describe("AIRunTranscript.clipArguments", () => {
|
|
58
|
+
test("keeps arguments under the cap verbatim", () => {
|
|
59
|
+
const args: { path: string } = { path: "src/Index.ts" };
|
|
60
|
+
expect(AIRunTranscript.clipArguments(args)).toEqual({
|
|
61
|
+
value: args,
|
|
62
|
+
isTruncated: false,
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
/*
|
|
67
|
+
* A write_file call carries a whole file in its arguments — the single
|
|
68
|
+
* likeliest way one tool call bloats a row.
|
|
69
|
+
*/
|
|
70
|
+
test("replaces oversized arguments with a marker that keeps a preview", () => {
|
|
71
|
+
const result: {
|
|
72
|
+
value: Record<string, unknown> | undefined;
|
|
73
|
+
isTruncated: boolean;
|
|
74
|
+
} = AIRunTranscript.clipArguments({
|
|
75
|
+
content: "y".repeat(MAX_TOOL_ARGUMENT_CHARS + 1000),
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
expect(result.isTruncated).toBe(true);
|
|
79
|
+
expect(result.value!["_note"]).toContain("exceeds");
|
|
80
|
+
expect(typeof result.value!["_preview"]).toBe("string");
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
describe("AIRunTranscript.clampPayload", () => {
|
|
85
|
+
test("caps each field independently and stamps isTruncated", () => {
|
|
86
|
+
const clamped: AIRunEventContentPayload = AIRunTranscript.clampPayload({
|
|
87
|
+
requestMessages: [
|
|
88
|
+
{ role: "user", content: "a".repeat(MAX_MESSAGE_CONTENT_CHARS + 500) },
|
|
89
|
+
],
|
|
90
|
+
responseContent: "b".repeat(MAX_RESPONSE_CONTENT_CHARS + 500),
|
|
91
|
+
toolResult: "c".repeat(MAX_TOOL_RESULT_CHARS + 500),
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
expect(clamped.isTruncated).toBe(true);
|
|
95
|
+
expect(clamped.requestMessages![0]!.content.length).toBeLessThanOrEqual(
|
|
96
|
+
MAX_MESSAGE_CONTENT_CHARS + 100,
|
|
97
|
+
);
|
|
98
|
+
expect(clamped.responseContent!.length).toBeLessThanOrEqual(
|
|
99
|
+
MAX_RESPONSE_CONTENT_CHARS + 100,
|
|
100
|
+
);
|
|
101
|
+
expect(clamped.toolResult!.length).toBeLessThanOrEqual(
|
|
102
|
+
MAX_TOOL_RESULT_CHARS + 100,
|
|
103
|
+
);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test("leaves a small payload entirely alone", () => {
|
|
107
|
+
const payload: AIRunEventContentPayload = {
|
|
108
|
+
requestMessages: [{ role: "user", content: "fix the null deref" }],
|
|
109
|
+
responseContent: "Reading the file.",
|
|
110
|
+
responseToolCalls: [{ name: "read_file", arguments: { path: "a.ts" } }],
|
|
111
|
+
totalTokens: 120,
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const clamped: AIRunEventContentPayload =
|
|
115
|
+
AIRunTranscript.clampPayload(payload);
|
|
116
|
+
|
|
117
|
+
expect(clamped.isTruncated).toBeUndefined();
|
|
118
|
+
expect(clamped).toEqual(payload);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
/*
|
|
122
|
+
* The regression this file exists for. A single-pass backstop that only
|
|
123
|
+
* dropped requestMessages "succeeded" by deleting a two-character user
|
|
124
|
+
* message while the row stayed ~1.5x over the ceiling, because the bulk was
|
|
125
|
+
* in responseToolCalls. The cap must hold regardless of which field is fat.
|
|
126
|
+
*/
|
|
127
|
+
test("holds the ceiling when the bulk is in responseToolCalls, not messages", () => {
|
|
128
|
+
const clamped: AIRunEventContentPayload = AIRunTranscript.clampPayload({
|
|
129
|
+
requestMessages: [{ role: "user", content: "hi" }],
|
|
130
|
+
responseToolCalls: Array.from(
|
|
131
|
+
{ length: 30 },
|
|
132
|
+
(_value: unknown, index: number) => {
|
|
133
|
+
return {
|
|
134
|
+
id: `call_${index}`,
|
|
135
|
+
name: "write_file",
|
|
136
|
+
arguments: { content: "z".repeat(MAX_TOOL_ARGUMENT_CHARS - 200) },
|
|
137
|
+
};
|
|
138
|
+
},
|
|
139
|
+
),
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
expect(serializedLength(clamped)).toBeLessThanOrEqual(MAX_PAYLOAD_CHARS);
|
|
143
|
+
expect(clamped.isTruncated).toBe(true);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test("holds the ceiling when the bulk is a huge tool result", () => {
|
|
147
|
+
const clamped: AIRunEventContentPayload = AIRunTranscript.clampPayload({
|
|
148
|
+
toolResult: "q".repeat(MAX_PAYLOAD_CHARS * 3),
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
expect(serializedLength(clamped)).toBeLessThanOrEqual(MAX_PAYLOAD_CHARS);
|
|
152
|
+
expect(clamped.isTruncated).toBe(true);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
/*
|
|
156
|
+
* Bounded ENTRIES are not a bounded ARRAY: with every argument already
|
|
157
|
+
* dropped, enough calls still blow the ceiling. The last resort collapses
|
|
158
|
+
* the array to a count.
|
|
159
|
+
*/
|
|
160
|
+
test("holds the ceiling against a pathological number of tool calls", () => {
|
|
161
|
+
const clamped: AIRunEventContentPayload = AIRunTranscript.clampPayload({
|
|
162
|
+
responseToolCalls: Array.from(
|
|
163
|
+
{ length: 20000 },
|
|
164
|
+
(_value: unknown, index: number) => {
|
|
165
|
+
return { id: `call_${index}`, name: "read_file" };
|
|
166
|
+
},
|
|
167
|
+
),
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
expect(serializedLength(clamped)).toBeLessThanOrEqual(MAX_PAYLOAD_CHARS);
|
|
171
|
+
expect(clamped.isTruncated).toBe(true);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
// A clipped prompt must never be presented as though it were whole.
|
|
175
|
+
test("never reports a clipped payload as complete", () => {
|
|
176
|
+
const clamped: AIRunEventContentPayload = AIRunTranscript.clampPayload({
|
|
177
|
+
responseContent: "r".repeat(MAX_RESPONSE_CONTENT_CHARS + 1),
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
expect(clamped.isTruncated).toBe(true);
|
|
181
|
+
});
|
|
182
|
+
});
|