@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,596 @@
|
|
|
1
|
+
import {
|
|
2
|
+
FindCodeForExceptionTool,
|
|
3
|
+
ListCodeRepositoriesTool,
|
|
4
|
+
ReadCodeFileTool,
|
|
5
|
+
SearchCodeTool,
|
|
6
|
+
} from "../../../../Server/Utils/AI/Toolbox/CodeTools";
|
|
7
|
+
import {
|
|
8
|
+
ToolContext,
|
|
9
|
+
ToolExecutionResult,
|
|
10
|
+
} from "../../../../Server/Utils/AI/Toolbox/ToolTypes";
|
|
11
|
+
import CodeRepositoryService from "../../../../Server/Services/CodeRepositoryService";
|
|
12
|
+
import TelemetryExceptionService from "../../../../Server/Services/TelemetryExceptionService";
|
|
13
|
+
import GitHubUtil from "../../../../Server/Utils/CodeRepository/GitHub/GitHub";
|
|
14
|
+
import CodeRepositoryModel from "../../../../Models/DatabaseModels/CodeRepository";
|
|
15
|
+
import TelemetryException from "../../../../Models/DatabaseModels/TelemetryException";
|
|
16
|
+
import CodeRepositoryType from "../../../../Types/CodeRepository/CodeRepositoryType";
|
|
17
|
+
import ObjectID from "../../../../Types/ObjectID";
|
|
18
|
+
import { describe, expect, test, afterEach } from "@jest/globals";
|
|
19
|
+
|
|
20
|
+
/*
|
|
21
|
+
* The code tools are the bridge from a telemetry signal to the source that
|
|
22
|
+
* produced it. These tests mock the repository/exception services and the
|
|
23
|
+
* GitHub Contents API (no network, no clone) to lock in the behaviours that
|
|
24
|
+
* are load-bearing for trust:
|
|
25
|
+
* (a) a repository that is not readable (GitLab, or a GitHub row whose App
|
|
26
|
+
* installation is gone) is invisible to the model — it must never look
|
|
27
|
+
* readable;
|
|
28
|
+
* (b) ambiguity between several repositories is an error the model can fix,
|
|
29
|
+
* never a guess at which codebase the question is about;
|
|
30
|
+
* (c) read_code_file's line window resolves and clamps correctly, and lines
|
|
31
|
+
* carry their real file line numbers so the model can cite them;
|
|
32
|
+
* (d) secrets in source are redacted before they egress to the provider;
|
|
33
|
+
* (e) an exception with no stack trace, or that matches no repository, is
|
|
34
|
+
* reported honestly instead of guessed at.
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
const ctx: ToolContext = {
|
|
38
|
+
projectId: ObjectID.generate(),
|
|
39
|
+
props: { isRoot: true },
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const REPO_ID: ObjectID = ObjectID.generate();
|
|
43
|
+
|
|
44
|
+
function buildRepository(data?: {
|
|
45
|
+
id?: ObjectID;
|
|
46
|
+
name?: string;
|
|
47
|
+
hostedAt?: CodeRepositoryType;
|
|
48
|
+
installationId?: string | undefined;
|
|
49
|
+
}): CodeRepositoryModel {
|
|
50
|
+
const repository: CodeRepositoryModel = new CodeRepositoryModel();
|
|
51
|
+
repository._id = (data?.id ?? REPO_ID).toString();
|
|
52
|
+
repository.name = data?.name ?? "checkout";
|
|
53
|
+
repository.organizationName = "acme";
|
|
54
|
+
repository.repositoryName = "checkout";
|
|
55
|
+
repository.mainBranchName = "main";
|
|
56
|
+
repository.repositoryHostedAt = data?.hostedAt ?? CodeRepositoryType.GitHub;
|
|
57
|
+
|
|
58
|
+
if (data?.installationId !== undefined) {
|
|
59
|
+
repository.gitHubAppInstallationId = data.installationId;
|
|
60
|
+
} else {
|
|
61
|
+
repository.gitHubAppInstallationId = "12345";
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return repository;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function mockRepositories(repositories: Array<CodeRepositoryModel>): void {
|
|
68
|
+
jest
|
|
69
|
+
.spyOn(CodeRepositoryService, "findBy")
|
|
70
|
+
.mockResolvedValue(repositories as never);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function mockFileContent(content: string): void {
|
|
74
|
+
const lines: Array<string> = content === "" ? [] : content.split("\n");
|
|
75
|
+
|
|
76
|
+
if (lines[lines.length - 1] === "") {
|
|
77
|
+
lines.pop();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
jest.spyOn(GitHubUtil, "getFileContent").mockResolvedValue({
|
|
81
|
+
content: content,
|
|
82
|
+
filePath: "src/billing/charge.ts",
|
|
83
|
+
sizeInBytes: content.length,
|
|
84
|
+
totalLines: lines.length,
|
|
85
|
+
htmlUrl: "https://github.com/acme/checkout/blob/main/src/billing/charge.ts",
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
afterEach(() => {
|
|
90
|
+
jest.restoreAllMocks();
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
describe("list_code_repositories", () => {
|
|
94
|
+
test("hides repositories that cannot actually be read", async () => {
|
|
95
|
+
mockRepositories([
|
|
96
|
+
buildRepository({ name: "readable" }),
|
|
97
|
+
// GitHub row whose App installation was uninstalled.
|
|
98
|
+
buildRepository({
|
|
99
|
+
id: ObjectID.generate(),
|
|
100
|
+
name: "uninstalled",
|
|
101
|
+
installationId: "",
|
|
102
|
+
}),
|
|
103
|
+
// GitLab has no read path yet.
|
|
104
|
+
buildRepository({
|
|
105
|
+
id: ObjectID.generate(),
|
|
106
|
+
name: "gitlab-repo",
|
|
107
|
+
hostedAt: CodeRepositoryType.GitLab,
|
|
108
|
+
}),
|
|
109
|
+
]);
|
|
110
|
+
|
|
111
|
+
const result: ToolExecutionResult = await ListCodeRepositoriesTool.execute(
|
|
112
|
+
{},
|
|
113
|
+
ctx,
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
expect(result.dataForLlm).toContain("readable");
|
|
117
|
+
expect(result.dataForLlm).not.toContain("uninstalled");
|
|
118
|
+
expect(result.dataForLlm).not.toContain("gitlab-repo");
|
|
119
|
+
expect(result.rowCount).toBe(1);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test("says plainly when no repository is connected", async () => {
|
|
123
|
+
mockRepositories([]);
|
|
124
|
+
|
|
125
|
+
const result: ToolExecutionResult = await ListCodeRepositoriesTool.execute(
|
|
126
|
+
{},
|
|
127
|
+
ctx,
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
expect(result.rowCount).toBe(0);
|
|
131
|
+
expect(result.dataForLlm).toContain(
|
|
132
|
+
"no GitHub-connected code repositories",
|
|
133
|
+
);
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
describe("read_code_file — repository targeting", () => {
|
|
138
|
+
test("refuses to guess when several repositories could match", async () => {
|
|
139
|
+
mockRepositories([
|
|
140
|
+
buildRepository({ id: REPO_ID, name: "checkout" }),
|
|
141
|
+
buildRepository({ id: ObjectID.generate(), name: "billing" }),
|
|
142
|
+
]);
|
|
143
|
+
|
|
144
|
+
await expect(
|
|
145
|
+
ReadCodeFileTool.execute({ filePath: "src/index.ts" }, ctx),
|
|
146
|
+
).rejects.toThrow(/repositoryId is required/);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
test("does not require repositoryId when exactly one repository exists", async () => {
|
|
150
|
+
mockRepositories([buildRepository()]);
|
|
151
|
+
mockFileContent("const a = 1;");
|
|
152
|
+
|
|
153
|
+
const result: ToolExecutionResult = await ReadCodeFileTool.execute(
|
|
154
|
+
{ filePath: "src/billing/charge.ts" },
|
|
155
|
+
ctx,
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
expect(result.dataForLlm).toContain("const a = 1;");
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
test("explains when the project has no connected repository at all", async () => {
|
|
162
|
+
mockRepositories([]);
|
|
163
|
+
|
|
164
|
+
await expect(
|
|
165
|
+
ReadCodeFileTool.execute({ filePath: "src/index.ts" }, ctx),
|
|
166
|
+
).rejects.toThrow(/no GitHub-connected code repositories/);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
test("reports a missing file as fixable rather than crashing", async () => {
|
|
170
|
+
mockRepositories([buildRepository()]);
|
|
171
|
+
jest.spyOn(GitHubUtil, "getFileContent").mockResolvedValue(null);
|
|
172
|
+
|
|
173
|
+
await expect(
|
|
174
|
+
ReadCodeFileTool.execute({ filePath: "src/nope.ts" }, ctx),
|
|
175
|
+
).rejects.toThrow(/No readable file at "src\/nope.ts"/);
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
describe("read_code_file — line windows", () => {
|
|
180
|
+
const file: string = Array.from({ length: 500 }, (_v: unknown, i: number) => {
|
|
181
|
+
return `line ${i + 1}`;
|
|
182
|
+
}).join("\n");
|
|
183
|
+
|
|
184
|
+
test("numbers lines with their real file line numbers", async () => {
|
|
185
|
+
mockRepositories([buildRepository()]);
|
|
186
|
+
mockFileContent(file);
|
|
187
|
+
|
|
188
|
+
const result: ToolExecutionResult = await ReadCodeFileTool.execute(
|
|
189
|
+
{ filePath: "src/billing/charge.ts", startLine: 10, endLine: 12 },
|
|
190
|
+
ctx,
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
expect(result.dataForLlm).toContain("10| line 10");
|
|
194
|
+
expect(result.dataForLlm).toContain("12| line 12");
|
|
195
|
+
expect(result.dataForLlm).not.toContain("line 13");
|
|
196
|
+
expect(result.dataForLlm).toContain("showing lines 10-12 of 500");
|
|
197
|
+
expect(result.rowCount).toBe(3);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
test("aroundLine centres a window on a stack-trace line", async () => {
|
|
201
|
+
mockRepositories([buildRepository()]);
|
|
202
|
+
mockFileContent(file);
|
|
203
|
+
|
|
204
|
+
const result: ToolExecutionResult = await ReadCodeFileTool.execute(
|
|
205
|
+
{ filePath: "src/billing/charge.ts", aroundLine: 200 },
|
|
206
|
+
ctx,
|
|
207
|
+
);
|
|
208
|
+
|
|
209
|
+
// 60 lines either side of 200.
|
|
210
|
+
expect(result.dataForLlm).toContain("showing lines 140-260 of 500");
|
|
211
|
+
expect(result.dataForLlm).toContain("200| line 200");
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
test("aroundLine near the top of a file does not run off the start", async () => {
|
|
215
|
+
mockRepositories([buildRepository()]);
|
|
216
|
+
mockFileContent(file);
|
|
217
|
+
|
|
218
|
+
const result: ToolExecutionResult = await ReadCodeFileTool.execute(
|
|
219
|
+
{ filePath: "src/billing/charge.ts", aroundLine: 3 },
|
|
220
|
+
ctx,
|
|
221
|
+
);
|
|
222
|
+
|
|
223
|
+
expect(result.dataForLlm).toContain("showing lines 1-63 of 500");
|
|
224
|
+
expect(result.dataForLlm).toContain("1| line 1");
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
test("caps an oversized window and tells the model how to read on", async () => {
|
|
228
|
+
mockRepositories([buildRepository()]);
|
|
229
|
+
mockFileContent(file);
|
|
230
|
+
|
|
231
|
+
const result: ToolExecutionResult = await ReadCodeFileTool.execute(
|
|
232
|
+
{ filePath: "src/billing/charge.ts", startLine: 1, endLine: 500 },
|
|
233
|
+
ctx,
|
|
234
|
+
);
|
|
235
|
+
|
|
236
|
+
expect(result.isTruncated).toBe(true);
|
|
237
|
+
expect(result.dataForLlm).toContain("showing lines 1-400 of 500");
|
|
238
|
+
// The hint must resume exactly where the read stopped — no skipped lines.
|
|
239
|
+
expect(result.dataForLlm).toContain("startLine=401");
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
test("redacts secrets found in source before they reach the provider", async () => {
|
|
243
|
+
mockRepositories([buildRepository()]);
|
|
244
|
+
mockFileContent(
|
|
245
|
+
'const stripeKey = "sk_live_x";\nconst password = "hunter2000";',
|
|
246
|
+
);
|
|
247
|
+
|
|
248
|
+
const result: ToolExecutionResult = await ReadCodeFileTool.execute(
|
|
249
|
+
{ filePath: "src/config.ts" },
|
|
250
|
+
ctx,
|
|
251
|
+
);
|
|
252
|
+
|
|
253
|
+
expect(result.dataForLlm).not.toContain("hunter2000");
|
|
254
|
+
expect(result.dataForLlm).toContain("[redacted]");
|
|
255
|
+
expect(result.redactionCount).toBeGreaterThan(0);
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
test("an explicit endLine survives being combined with aroundLine", async () => {
|
|
259
|
+
mockRepositories([buildRepository()]);
|
|
260
|
+
mockFileContent(file);
|
|
261
|
+
|
|
262
|
+
const result: ToolExecutionResult = await ReadCodeFileTool.execute(
|
|
263
|
+
{ filePath: "src/billing/charge.ts", aroundLine: 200, endLine: 210 },
|
|
264
|
+
ctx,
|
|
265
|
+
);
|
|
266
|
+
|
|
267
|
+
expect(result.dataForLlm).toContain("showing lines 140-210 of 500");
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
test("a trailing newline does not invent a phantom last line", async () => {
|
|
271
|
+
mockRepositories([buildRepository()]);
|
|
272
|
+
mockFileContent("alpha\nbravo\n");
|
|
273
|
+
|
|
274
|
+
const result: ToolExecutionResult = await ReadCodeFileTool.execute(
|
|
275
|
+
{ filePath: "src/two.ts" },
|
|
276
|
+
ctx,
|
|
277
|
+
);
|
|
278
|
+
|
|
279
|
+
expect(result.dataForLlm).toContain("showing lines 1-2 of 2");
|
|
280
|
+
expect(result.rowCount).toBe(2);
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
test("an empty file reads as empty, not as an error", async () => {
|
|
284
|
+
mockRepositories([buildRepository()]);
|
|
285
|
+
mockFileContent("");
|
|
286
|
+
|
|
287
|
+
const result: ToolExecutionResult = await ReadCodeFileTool.execute(
|
|
288
|
+
{ filePath: "src/empty.ts" },
|
|
289
|
+
ctx,
|
|
290
|
+
);
|
|
291
|
+
|
|
292
|
+
expect(result.rowCount).toBe(0);
|
|
293
|
+
expect(result.dataForLlm).toContain("this file is empty");
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
/*
|
|
297
|
+
* The bug this locks: the serializer hard-slices at its own 16KB payload cap.
|
|
298
|
+
* If the window is trimmed only by line count, the header advertises a range
|
|
299
|
+
* the payload does not contain, and the "read on" hint then skips exactly the
|
|
300
|
+
* lines the slice ate.
|
|
301
|
+
*/
|
|
302
|
+
test("reports the range it actually returned when lines are long", async () => {
|
|
303
|
+
mockRepositories([buildRepository()]);
|
|
304
|
+
// 400 lines x ~200 bytes = ~80KB, far over the byte budget.
|
|
305
|
+
const fat: string = Array.from(
|
|
306
|
+
{ length: 400 },
|
|
307
|
+
(_v: unknown, i: number) => {
|
|
308
|
+
return `${i + 1}:${"x".repeat(200)}`;
|
|
309
|
+
},
|
|
310
|
+
).join("\n");
|
|
311
|
+
mockFileContent(fat);
|
|
312
|
+
|
|
313
|
+
const result: ToolExecutionResult = await ReadCodeFileTool.execute(
|
|
314
|
+
{ filePath: "src/fat.ts", startLine: 1, endLine: 400 },
|
|
315
|
+
ctx,
|
|
316
|
+
);
|
|
317
|
+
|
|
318
|
+
expect(result.isTruncated).toBe(true);
|
|
319
|
+
|
|
320
|
+
// The advertised range must match what is really in the payload.
|
|
321
|
+
const header: RegExpMatchArray | null = result.dataForLlm.match(
|
|
322
|
+
/showing lines 1-(\d+) of 400/,
|
|
323
|
+
);
|
|
324
|
+
expect(header).not.toBeNull();
|
|
325
|
+
const advertisedEnd: number = Number(header![1]);
|
|
326
|
+
expect(advertisedEnd).toBeLessThan(400);
|
|
327
|
+
expect(result.rowCount).toBe(advertisedEnd);
|
|
328
|
+
expect(result.dataForLlm).toContain(`${advertisedEnd}| `);
|
|
329
|
+
expect(result.dataForLlm).not.toContain(`${advertisedEnd + 1}| `);
|
|
330
|
+
|
|
331
|
+
// The pagination hint must resume exactly where it stopped — no skipped lines.
|
|
332
|
+
expect(result.dataForLlm).toContain(`startLine=${advertisedEnd + 1}`);
|
|
333
|
+
});
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
describe("search_code", () => {
|
|
337
|
+
test("returns shortest paths first so the likely file is not cut off", async () => {
|
|
338
|
+
mockRepositories([buildRepository()]);
|
|
339
|
+
jest
|
|
340
|
+
.spyOn(GitHubUtil, "getRepositoryTreePaths")
|
|
341
|
+
.mockResolvedValue([
|
|
342
|
+
"src/billing/__tests__/charge.fixture.ts",
|
|
343
|
+
"src/billing/charge.ts",
|
|
344
|
+
"docs/unrelated.md",
|
|
345
|
+
]);
|
|
346
|
+
|
|
347
|
+
const result: ToolExecutionResult = await SearchCodeTool.execute(
|
|
348
|
+
{ query: "charge" },
|
|
349
|
+
ctx,
|
|
350
|
+
);
|
|
351
|
+
|
|
352
|
+
const firstIndex: number = result.dataForLlm.indexOf(
|
|
353
|
+
"src/billing/charge.ts",
|
|
354
|
+
);
|
|
355
|
+
const fixtureIndex: number = result.dataForLlm.indexOf("charge.fixture.ts");
|
|
356
|
+
|
|
357
|
+
expect(firstIndex).toBeGreaterThanOrEqual(0);
|
|
358
|
+
expect(firstIndex).toBeLessThan(fixtureIndex);
|
|
359
|
+
expect(result.dataForLlm).not.toContain("docs/unrelated.md");
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
test("reports no matches honestly", async () => {
|
|
363
|
+
mockRepositories([buildRepository()]);
|
|
364
|
+
jest
|
|
365
|
+
.spyOn(GitHubUtil, "getRepositoryTreePaths")
|
|
366
|
+
.mockResolvedValue(["src/billing/charge.ts"]);
|
|
367
|
+
|
|
368
|
+
const result: ToolExecutionResult = await SearchCodeTool.execute(
|
|
369
|
+
{ query: "nonexistent" },
|
|
370
|
+
ctx,
|
|
371
|
+
);
|
|
372
|
+
|
|
373
|
+
expect(result.rowCount).toBe(0);
|
|
374
|
+
expect(result.dataForLlm).toContain("no file paths");
|
|
375
|
+
});
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
describe("find_code_for_exception", () => {
|
|
379
|
+
function mockException(
|
|
380
|
+
stackTrace: string | undefined,
|
|
381
|
+
message?: string,
|
|
382
|
+
): void {
|
|
383
|
+
const exception: TelemetryException = new TelemetryException();
|
|
384
|
+
exception._id = ObjectID.generate().toString();
|
|
385
|
+
exception.message = message ?? "Cannot read property 'id' of undefined";
|
|
386
|
+
exception.exceptionType = "TypeError";
|
|
387
|
+
exception.occuranceCount = 42;
|
|
388
|
+
|
|
389
|
+
if (stackTrace !== undefined) {
|
|
390
|
+
exception.stackTrace = stackTrace;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
jest
|
|
394
|
+
.spyOn(TelemetryExceptionService, "findOneById")
|
|
395
|
+
.mockResolvedValue(exception as never);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function mockResolution(): void {
|
|
399
|
+
jest
|
|
400
|
+
.spyOn(CodeRepositoryService, "resolveRepositoryForException")
|
|
401
|
+
.mockResolvedValue({
|
|
402
|
+
codeRepositoryId: REPO_ID.toString(),
|
|
403
|
+
organizationName: "acme",
|
|
404
|
+
repositoryName: "checkout",
|
|
405
|
+
servicePathInRepository: null,
|
|
406
|
+
method: "stack-trace",
|
|
407
|
+
evidence: "Matched src/billing/charge.ts in acme/checkout",
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
test("maps a stack trace to a repository and its implicated frames", async () => {
|
|
412
|
+
mockException(
|
|
413
|
+
[
|
|
414
|
+
"TypeError: Cannot read property 'id' of undefined",
|
|
415
|
+
" at chargeCustomer (/app/src/billing/charge.ts:42:15)",
|
|
416
|
+
" at processPayment (/app/node_modules/express/lib/router.js:10:5)",
|
|
417
|
+
].join("\n"),
|
|
418
|
+
);
|
|
419
|
+
|
|
420
|
+
mockRepositories([buildRepository()]);
|
|
421
|
+
mockResolution();
|
|
422
|
+
jest
|
|
423
|
+
.spyOn(GitHubUtil, "getRepositoryTreePaths")
|
|
424
|
+
.mockResolvedValue(["src/billing/charge.ts", "src/index.ts"]);
|
|
425
|
+
|
|
426
|
+
const result: ToolExecutionResult = await FindCodeForExceptionTool.execute(
|
|
427
|
+
{ exceptionId: ObjectID.generate().toString() },
|
|
428
|
+
ctx,
|
|
429
|
+
);
|
|
430
|
+
|
|
431
|
+
expect(result.dataForLlm).toContain("acme/checkout");
|
|
432
|
+
expect(result.dataForLlm).toContain(REPO_ID.toString());
|
|
433
|
+
expect(result.dataForLlm).toContain("stack-trace");
|
|
434
|
+
expect(result.dataForLlm).toContain("line=42");
|
|
435
|
+
// Library frames are dropped when application frames exist.
|
|
436
|
+
expect(result.dataForLlm).not.toContain("router.js");
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
/*
|
|
440
|
+
* The bug this locks: StackTraceParser reports the RUNTIME path
|
|
441
|
+
* (/app/src/billing/charge.ts), but the Contents API needs the REPOSITORY
|
|
442
|
+
* path (src/billing/charge.ts). Emitting the runtime path made every
|
|
443
|
+
* read_code_file follow-up 404 — the feature's whole point.
|
|
444
|
+
*/
|
|
445
|
+
test("rewrites runtime stack-trace paths to real repository paths", async () => {
|
|
446
|
+
mockException(" at chargeCustomer (/app/src/billing/charge.ts:42:15)");
|
|
447
|
+
mockRepositories([buildRepository()]);
|
|
448
|
+
mockResolution();
|
|
449
|
+
jest
|
|
450
|
+
.spyOn(GitHubUtil, "getRepositoryTreePaths")
|
|
451
|
+
.mockResolvedValue(["src/billing/charge.ts"]);
|
|
452
|
+
|
|
453
|
+
const result: ToolExecutionResult = await FindCodeForExceptionTool.execute(
|
|
454
|
+
{ exceptionId: ObjectID.generate().toString() },
|
|
455
|
+
ctx,
|
|
456
|
+
);
|
|
457
|
+
|
|
458
|
+
expect(result.dataForLlm).toContain("file=src/billing/charge.ts");
|
|
459
|
+
expect(result.dataForLlm).not.toContain("/app/src/billing/charge.ts");
|
|
460
|
+
expect(result.dataForLlm).toContain("openableWithReadCodeFile=true");
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
test("marks a frame unopenable rather than guessing when it is not in the tree", async () => {
|
|
464
|
+
mockException(" at helper (/app/src/mystery/ghost.ts:7:1)");
|
|
465
|
+
mockRepositories([buildRepository()]);
|
|
466
|
+
mockResolution();
|
|
467
|
+
jest
|
|
468
|
+
.spyOn(GitHubUtil, "getRepositoryTreePaths")
|
|
469
|
+
.mockResolvedValue(["src/billing/charge.ts"]);
|
|
470
|
+
|
|
471
|
+
const result: ToolExecutionResult = await FindCodeForExceptionTool.execute(
|
|
472
|
+
{ exceptionId: ObjectID.generate().toString() },
|
|
473
|
+
ctx,
|
|
474
|
+
);
|
|
475
|
+
|
|
476
|
+
expect(result.dataForLlm).not.toContain("openableWithReadCodeFile=true");
|
|
477
|
+
expect(result.dataForLlm).toContain(
|
|
478
|
+
"None of these frames could be matched",
|
|
479
|
+
);
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
test("refuses to pick between two equally-plausible tree matches", async () => {
|
|
483
|
+
mockException(" at handler (/app/charge.ts:9:1)");
|
|
484
|
+
mockRepositories([buildRepository()]);
|
|
485
|
+
mockResolution();
|
|
486
|
+
jest
|
|
487
|
+
.spyOn(GitHubUtil, "getRepositoryTreePaths")
|
|
488
|
+
.mockResolvedValue(["src/billing/charge.ts", "src/legacy/charge.ts"]);
|
|
489
|
+
|
|
490
|
+
const result: ToolExecutionResult = await FindCodeForExceptionTool.execute(
|
|
491
|
+
{ exceptionId: ObjectID.generate().toString() },
|
|
492
|
+
ctx,
|
|
493
|
+
);
|
|
494
|
+
|
|
495
|
+
expect(result.dataForLlm).not.toContain("openableWithReadCodeFile=true");
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
/*
|
|
499
|
+
* exception.message is populated verbatim from the monitored app's thrown
|
|
500
|
+
* error — the most attacker-influenceable string in the tool belt. It must
|
|
501
|
+
* get the same redaction top_exceptions applies to the identical field.
|
|
502
|
+
*/
|
|
503
|
+
test("redacts secrets in the exception message before they egress", async () => {
|
|
504
|
+
mockException(
|
|
505
|
+
" at auth (/app/src/auth.ts:1:1)",
|
|
506
|
+
"auth failed for admin@acme.com password=hunter2xyz key AKIAIOSFODNN7EXAMPLE",
|
|
507
|
+
);
|
|
508
|
+
mockRepositories([buildRepository()]);
|
|
509
|
+
mockResolution();
|
|
510
|
+
jest.spyOn(GitHubUtil, "getRepositoryTreePaths").mockResolvedValue([]);
|
|
511
|
+
|
|
512
|
+
const result: ToolExecutionResult = await FindCodeForExceptionTool.execute(
|
|
513
|
+
{ exceptionId: ObjectID.generate().toString() },
|
|
514
|
+
ctx,
|
|
515
|
+
);
|
|
516
|
+
|
|
517
|
+
expect(result.dataForLlm).not.toContain("admin@acme.com");
|
|
518
|
+
expect(result.dataForLlm).not.toContain("hunter2xyz");
|
|
519
|
+
expect(result.dataForLlm).not.toContain("AKIAIOSFODNN7EXAMPLE");
|
|
520
|
+
expect(result.redactionCount).toBeGreaterThan(0);
|
|
521
|
+
});
|
|
522
|
+
|
|
523
|
+
test("redacts the exception message on the no-stack-trace path too", async () => {
|
|
524
|
+
mockException(undefined, "boom for admin@acme.com");
|
|
525
|
+
|
|
526
|
+
const result: ToolExecutionResult = await FindCodeForExceptionTool.execute(
|
|
527
|
+
{ exceptionId: ObjectID.generate().toString() },
|
|
528
|
+
ctx,
|
|
529
|
+
);
|
|
530
|
+
|
|
531
|
+
expect(result.dataForLlm).not.toContain("admin@acme.com");
|
|
532
|
+
expect(result.redactionCount).toBeGreaterThan(0);
|
|
533
|
+
});
|
|
534
|
+
|
|
535
|
+
/*
|
|
536
|
+
* resolveRepositoryForException queries as isRoot (it serves the autonomous
|
|
537
|
+
* agent). A label-restricted user must not learn a repository's identity
|
|
538
|
+
* through it.
|
|
539
|
+
*/
|
|
540
|
+
test("does not disclose a repository the user cannot read", async () => {
|
|
541
|
+
mockException(" at chargeCustomer (/app/src/billing/charge.ts:42:15)");
|
|
542
|
+
mockResolution();
|
|
543
|
+
// The user's own readable set is empty — the label ACL excludes this repo.
|
|
544
|
+
mockRepositories([]);
|
|
545
|
+
|
|
546
|
+
const result: ToolExecutionResult = await FindCodeForExceptionTool.execute(
|
|
547
|
+
{ exceptionId: ObjectID.generate().toString() },
|
|
548
|
+
ctx,
|
|
549
|
+
);
|
|
550
|
+
|
|
551
|
+
expect(result.dataForLlm).toContain("resolvedRepository: none");
|
|
552
|
+
expect(result.dataForLlm).not.toContain("acme/checkout");
|
|
553
|
+
expect(result.dataForLlm).not.toContain(REPO_ID.toString());
|
|
554
|
+
});
|
|
555
|
+
|
|
556
|
+
test("says the code could not be located rather than guessing a repository", async () => {
|
|
557
|
+
mockException(" at chargeCustomer (/app/src/billing/charge.ts:42:15)");
|
|
558
|
+
|
|
559
|
+
jest
|
|
560
|
+
.spyOn(CodeRepositoryService, "resolveRepositoryForException")
|
|
561
|
+
.mockResolvedValue(null);
|
|
562
|
+
|
|
563
|
+
const result: ToolExecutionResult = await FindCodeForExceptionTool.execute(
|
|
564
|
+
{ exceptionId: ObjectID.generate().toString() },
|
|
565
|
+
ctx,
|
|
566
|
+
);
|
|
567
|
+
|
|
568
|
+
expect(result.dataForLlm).toContain("resolvedRepository: none");
|
|
569
|
+
expect(result.dataForLlm).toContain("rather than guessing");
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
test("an exception with no stack trace is reported honestly", async () => {
|
|
573
|
+
mockException(undefined);
|
|
574
|
+
|
|
575
|
+
const result: ToolExecutionResult = await FindCodeForExceptionTool.execute(
|
|
576
|
+
{ exceptionId: ObjectID.generate().toString() },
|
|
577
|
+
ctx,
|
|
578
|
+
);
|
|
579
|
+
|
|
580
|
+
expect(result.rowCount).toBe(0);
|
|
581
|
+
expect(result.dataForLlm).toContain("no stack trace recorded");
|
|
582
|
+
});
|
|
583
|
+
|
|
584
|
+
test("a missing exception is an error the model can act on", async () => {
|
|
585
|
+
jest
|
|
586
|
+
.spyOn(TelemetryExceptionService, "findOneById")
|
|
587
|
+
.mockResolvedValue(null as never);
|
|
588
|
+
|
|
589
|
+
await expect(
|
|
590
|
+
FindCodeForExceptionTool.execute(
|
|
591
|
+
{ exceptionId: ObjectID.generate().toString() },
|
|
592
|
+
ctx,
|
|
593
|
+
),
|
|
594
|
+
).rejects.toThrow(/No exception found/);
|
|
595
|
+
});
|
|
596
|
+
});
|