@skyramp/mcp 0.2.7 → 0.2.8-rc.1
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/build/index.js +5 -2
- package/build/prompts/enhance-assertions/uiAssertionsPrompt.js +24 -5
- package/build/prompts/sut-setup/modes/adaptWorkflowPrompt.d.ts +2 -0
- package/build/prompts/sut-setup/modes/adaptWorkflowPrompt.js +114 -0
- package/build/prompts/sut-setup/modes/dockerComposePrompt.d.ts +2 -0
- package/build/prompts/sut-setup/modes/dockerComposePrompt.js +292 -0
- package/build/prompts/sut-setup/shared.d.ts +39 -0
- package/build/prompts/sut-setup/shared.js +132 -0
- package/build/prompts/test-maintenance/driftAnalysisSections.js +9 -3
- package/build/prompts/test-recommendation/analysisOutputPrompt.js +1 -1
- package/build/prompts/test-recommendation/diffExecutionPlan.js +76 -2
- package/build/prompts/test-recommendation/test-recommendation-prompt.test.js +91 -0
- package/build/prompts/testbot/testbot-prompts.d.ts +16 -1
- package/build/prompts/testbot/testbot-prompts.js +113 -22
- package/build/prompts/testbot/testbot-prompts.test.js +95 -45
- package/build/resources/sutSetupResource.d.ts +2 -0
- package/build/resources/sutSetupResource.js +45 -0
- package/build/resources/testbotResource.js +2 -2
- package/build/services/TestDiscoveryService.d.ts +2 -2
- package/build/services/TestDiscoveryService.js +22 -12
- package/build/services/TestExecutionService.d.ts +10 -1
- package/build/services/TestExecutionService.js +158 -26
- package/build/services/TestExecutionService.test.js +306 -0
- package/build/tools/executeSkyrampTestTool.js +26 -7
- package/build/tools/generate-tests/generateBatchScenarioRestTool.js +1 -1
- package/build/tools/submitReportTool.d.ts +10 -0
- package/build/tools/submitReportTool.js +106 -39
- package/build/tools/submitReportTool.test.js +296 -50
- package/build/tools/test-management/actionsTool.js +21 -7
- package/build/tools/test-management/analyzeChangesTool.d.ts +1 -1
- package/build/tools/test-management/analyzeChangesTool.js +106 -41
- package/build/tools/test-management/analyzeChangesTool.test.js +21 -41
- package/build/tools/test-management/analyzeTestHealthTool.js +16 -9
- package/build/tools/workspace/initializeWorkspaceTool.js +39 -22
- package/build/tools/workspace/serviceUpsert.d.ts +12 -0
- package/build/tools/workspace/serviceUpsert.js +23 -0
- package/build/tools/workspace/serviceUpsert.test.d.ts +1 -0
- package/build/tools/workspace/serviceUpsert.test.js +50 -0
- package/build/types/TestAnalysis.d.ts +2 -1
- package/build/types/TestExecution.d.ts +12 -2
- package/build/types/TestExecution.js +11 -1
- package/build/types/TestbotReport.d.ts +68 -0
- package/build/types/TestbotReport.js +1 -0
- package/build/types/index.d.ts +4 -0
- package/build/types/index.js +3 -0
- package/build/utils/AnalysisStateManager.d.ts +61 -1
- package/build/utils/AnalysisStateManager.js +144 -3
- package/build/utils/AnalysisStateManager.test.js +98 -0
- package/build/utils/docker.test.js +1 -1
- package/build/utils/routeParsers.d.ts +0 -10
- package/build/utils/routeParsers.js +31 -38
- package/build/utils/scenarioDrafting.d.ts +1 -1
- package/build/utils/scenarioDrafting.js +23 -1
- package/build/utils/utils.d.ts +7 -1
- package/build/utils/utils.js +10 -0
- package/build/utils/versions.d.ts +3 -3
- package/build/utils/versions.js +1 -1
- package/build/workspace/workspace.d.ts +10 -10
- package/build/workspace/workspace.js +7 -7
- package/build/workspace/workspace.test.js +6 -6
- package/node_modules/playwright/lib/mcp/skyramp/loadTraceTool.js +31 -13
- package/node_modules/playwright/lib/mcp/skyramp/traceRecordingBackend.js +88 -15
- package/node_modules/playwright/lib/mcp/test/skyRampExport.js +4 -3
- package/node_modules/playwright/node_modules/playwright-core/ThirdPartyNotices.txt +23 -126
- package/node_modules/playwright/node_modules/playwright-core/lib/generated/injectedScriptSource.js +1 -1
- package/node_modules/playwright/node_modules/playwright-core/lib/server/recorder/recorderApp.js +79 -19
- package/node_modules/playwright/node_modules/playwright-core/lib/server/recorder/skyramp/aliasRemapper.js +84 -0
- package/node_modules/playwright/node_modules/playwright-core/lib/server/recorder/skyramp/replayEngine.js +15 -4
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/recorder/assets/{codeMirrorModule-CPZKmjFR.js → codeMirrorModule-D0BjbCb7.js} +1 -1
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/recorder/assets/index-lvTRGFx-.js +193 -0
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/recorder/index.html +1 -1
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/assets/{codeMirrorModule-B0JOjboO.js → codeMirrorModule-Cqp3cwEJ.js} +1 -1
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/assets/{defaultSettingsView-1anWeyDf.js → defaultSettingsView-CYf9adZh.js} +79 -79
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/{index.Cc5029a3.js → index.B_7ywgmr.js} +1 -1
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/index.html +2 -2
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/uiMode.html +2 -2
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/{uiMode.Wo5yvvVh.js → uiMode.u4_8VnCV.js} +1 -1
- package/node_modules/playwright/node_modules/playwright-core/package.json +1 -1
- package/node_modules/playwright/node_modules/playwright-core/src/generated/injectedScriptSource.ts +1 -1
- package/node_modules/playwright/node_modules/playwright-core/src/server/recorder/recorderApp.ts +129 -43
- package/node_modules/playwright/node_modules/playwright-core/src/server/recorder/skyramp/aliasRemapper.ts +119 -0
- package/node_modules/playwright/node_modules/playwright-core/src/server/recorder/skyramp/replayEngine.ts +33 -10
- package/node_modules/playwright/package.json +1 -1
- package/package.json +10 -3
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/.bin/node-which +0 -52
- package/node_modules/playwright/node_modules/playwright-core/bundles/utils/node_modules/.bin/is-docker +0 -5
- package/node_modules/playwright/node_modules/playwright-core/bundles/utils/node_modules/.bin/mime +0 -46
- package/node_modules/playwright/node_modules/playwright-core/bundles/utils/node_modules/.bin/yaml +0 -11
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/recorder/assets/index-CsgPR2wy.js +0 -193
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/index.CP3Y8Zsb.js +0 -2
|
@@ -2,11 +2,12 @@
|
|
|
2
2
|
import { jest } from "@jest/globals";
|
|
3
3
|
import { TestType } from "../types/TestTypes.js";
|
|
4
4
|
import { DriftAction } from "../types/TestAnalysis.js";
|
|
5
|
+
import { TestExecutionStatus } from "../types/TestExecution.js";
|
|
5
6
|
import * as fs from "fs/promises";
|
|
6
7
|
import * as path from "path";
|
|
7
8
|
import * as os from "os";
|
|
8
9
|
jest.unstable_mockModule("../utils/logger.js", () => ({
|
|
9
|
-
logger: { info: jest.fn(), error: jest.fn() },
|
|
10
|
+
logger: { info: jest.fn(), error: jest.fn(), warning: jest.fn(), debug: jest.fn() },
|
|
10
11
|
}));
|
|
11
12
|
jest.unstable_mockModule("../services/AnalyticsService.js", () => ({
|
|
12
13
|
AnalyticsService: { pushMCPToolEvent: jest.fn().mockResolvedValue(undefined) },
|
|
@@ -25,14 +26,20 @@ function captureToolHandler() {
|
|
|
25
26
|
registerSubmitReportTool(fakeServer);
|
|
26
27
|
return handler;
|
|
27
28
|
}
|
|
28
|
-
function
|
|
29
|
+
async function writeStateFile(dir, existingTests) {
|
|
30
|
+
const stateFile = path.join(dir, "analyze-changes-state.json");
|
|
31
|
+
await fs.writeFile(stateFile, JSON.stringify({ existingTests }), "utf-8");
|
|
32
|
+
return stateFile;
|
|
33
|
+
}
|
|
34
|
+
function sampleReportParams(outputFile, stateFile) {
|
|
29
35
|
return {
|
|
30
36
|
summaryOutputFile: outputFile,
|
|
37
|
+
stateFile,
|
|
31
38
|
businessCaseAnalysis: "This PR adds product search. Tests needed for filtering.",
|
|
32
39
|
newTestsCreated: [
|
|
33
40
|
{ testId: "smoke-get-products-search", testType: TestType.SMOKE, category: "workflow", endpoint: "GET /api/v1/products/search", fileName: "search_smoke_test.py", reasoning: "Validates product search returns correct results for category filter" },
|
|
34
41
|
],
|
|
35
|
-
testMaintenance: [{ action: "UPDATE", testType: TestType.CONTRACT, endpoint: "GET /api/v1/products", fileName: "products_contract_test.py", description: "Updated auth flow in existing tests",
|
|
42
|
+
testMaintenance: [{ action: "UPDATE", testType: TestType.CONTRACT, endpoint: "GET /api/v1/products", fileName: "products_contract_test.py", description: "Updated auth flow in existing tests", beforeDetails: "", afterDetails: "" }],
|
|
36
43
|
testResults: [
|
|
37
44
|
{ testType: TestType.SMOKE, endpoint: "GET /api/v1/products", status: "Pass", details: "2.1s, products_smoke_test.py" },
|
|
38
45
|
{ testType: TestType.SMOKE, endpoint: "GET /api/v1/products/search", status: "Fail", details: "3.4s, search_smoke_test.py" },
|
|
@@ -67,7 +74,8 @@ describe("registerSubmitReportTool", () => {
|
|
|
67
74
|
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
68
75
|
tmpDirs.push(tmpDir);
|
|
69
76
|
const outputFile = path.join(tmpDir, "report.json");
|
|
70
|
-
const
|
|
77
|
+
const stateFile = await writeStateFile(tmpDir, []);
|
|
78
|
+
const result = await handler(sampleReportParams(outputFile, stateFile));
|
|
71
79
|
expect(result.isError).toBeUndefined();
|
|
72
80
|
expect(result.content[0].text).toContain("Report submitted successfully");
|
|
73
81
|
const written = JSON.parse(await fs.readFile(outputFile, "utf-8"));
|
|
@@ -77,11 +85,44 @@ describe("registerSubmitReportTool", () => {
|
|
|
77
85
|
expect(written.issuesFound).toHaveLength(1);
|
|
78
86
|
expect(written.testMaintenance).toHaveLength(1);
|
|
79
87
|
});
|
|
88
|
+
it("accepts and persists the per-item `repository` field", async () => {
|
|
89
|
+
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
90
|
+
tmpDirs.push(tmpDir);
|
|
91
|
+
const outputFile = path.join(tmpDir, "report.json");
|
|
92
|
+
const stateFile = await writeStateFile(tmpDir, []);
|
|
93
|
+
const params = sampleReportParams(outputFile, stateFile);
|
|
94
|
+
// Attribute one item to a related repo, leave others on the primary.
|
|
95
|
+
params.newTestsCreated[0] = { ...params.newTestsCreated[0], repository: "acme/backend" };
|
|
96
|
+
params.testResults[0] = { ...params.testResults[0], repository: "acme/backend" };
|
|
97
|
+
params.issuesFound[0] = { ...params.issuesFound[0], repository: "acme/backend" };
|
|
98
|
+
const result = await handler(params);
|
|
99
|
+
expect(result.isError).toBeUndefined();
|
|
100
|
+
const written = JSON.parse(await fs.readFile(outputFile, "utf-8"));
|
|
101
|
+
expect(written.newTestsCreated[0].repository).toBe("acme/backend");
|
|
102
|
+
expect(written.testResults[0].repository).toBe("acme/backend");
|
|
103
|
+
expect(written.issuesFound[0].repository).toBe("acme/backend");
|
|
104
|
+
// Items without a repository stay primary (undefined → omitted from JSON).
|
|
105
|
+
expect(written.testResults[1].repository).toBeUndefined();
|
|
106
|
+
});
|
|
107
|
+
it("normalizes a blank/whitespace-only `repository` to undefined", async () => {
|
|
108
|
+
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
109
|
+
tmpDirs.push(tmpDir);
|
|
110
|
+
const outputFile = path.join(tmpDir, "report.json");
|
|
111
|
+
const stateFile = await writeStateFile(tmpDir, []);
|
|
112
|
+
const params = sampleReportParams(outputFile, stateFile);
|
|
113
|
+
params.testResults[0] = { ...params.testResults[0], repository: " " };
|
|
114
|
+
const result = await handler(params);
|
|
115
|
+
expect(result.isError).toBeUndefined();
|
|
116
|
+
const written = JSON.parse(await fs.readFile(outputFile, "utf-8"));
|
|
117
|
+
// Blank string must not serialize as a distinct empty repo.
|
|
118
|
+
expect(written.testResults[0].repository).toBeUndefined();
|
|
119
|
+
});
|
|
80
120
|
it("should create parent directories if they don't exist", async () => {
|
|
81
121
|
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
82
122
|
tmpDirs.push(tmpDir);
|
|
83
123
|
const outputFile = path.join(tmpDir, "nested", "dirs", "report.json");
|
|
84
|
-
const
|
|
124
|
+
const stateFile = await writeStateFile(tmpDir, []);
|
|
125
|
+
const result = await handler(sampleReportParams(outputFile, stateFile));
|
|
85
126
|
expect(result.isError).toBeUndefined();
|
|
86
127
|
const written = JSON.parse(await fs.readFile(outputFile, "utf-8"));
|
|
87
128
|
expect(written.businessCaseAnalysis).toBeDefined();
|
|
@@ -90,11 +131,12 @@ describe("registerSubmitReportTool", () => {
|
|
|
90
131
|
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
91
132
|
tmpDirs.push(tmpDir);
|
|
92
133
|
const outputFile = path.join(tmpDir, "report.json");
|
|
134
|
+
const stateFile = await writeStateFile(tmpDir, []);
|
|
93
135
|
const result = await handler({
|
|
94
136
|
summaryOutputFile: outputFile,
|
|
137
|
+
stateFile,
|
|
95
138
|
businessCaseAnalysis: "Simple config change.",
|
|
96
139
|
newTestsCreated: [],
|
|
97
|
-
testMaintenance: [],
|
|
98
140
|
testResults: [
|
|
99
141
|
{ testType: TestType.SMOKE, endpoint: "GET /api/v1/products", status: "Pass", details: "1.0s, test.py" },
|
|
100
142
|
],
|
|
@@ -103,12 +145,15 @@ describe("registerSubmitReportTool", () => {
|
|
|
103
145
|
expect(result.isError).toBeUndefined();
|
|
104
146
|
const written = JSON.parse(await fs.readFile(outputFile, "utf-8"));
|
|
105
147
|
expect(written.newTestsCreated).toEqual([]);
|
|
106
|
-
expect(written.testMaintenance).
|
|
148
|
+
expect(written.testMaintenance).toBeUndefined();
|
|
107
149
|
expect(written.issuesFound).toEqual([]);
|
|
108
150
|
expect(written.testResults).toHaveLength(1);
|
|
109
151
|
});
|
|
110
152
|
it("should return error for invalid file path", async () => {
|
|
111
|
-
const
|
|
153
|
+
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
154
|
+
tmpDirs.push(tmpDir);
|
|
155
|
+
const stateFile = await writeStateFile(tmpDir, []);
|
|
156
|
+
const result = await handler(sampleReportParams("/nonexistent/readonly/path/report.json", stateFile));
|
|
112
157
|
expect(result.isError).toBe(true);
|
|
113
158
|
expect(result.content[0].text).toContain("Failed to write report");
|
|
114
159
|
});
|
|
@@ -116,8 +161,9 @@ describe("registerSubmitReportTool", () => {
|
|
|
116
161
|
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
117
162
|
tmpDirs.push(tmpDir);
|
|
118
163
|
const outputFile = path.join(tmpDir, "report.json");
|
|
164
|
+
const stateFile = await writeStateFile(tmpDir, []);
|
|
119
165
|
const result = await handler({
|
|
120
|
-
...sampleReportParams(outputFile),
|
|
166
|
+
...sampleReportParams(outputFile, stateFile),
|
|
121
167
|
commitMessage: "add contract tests for /products endpoint",
|
|
122
168
|
});
|
|
123
169
|
expect(result.isError).toBeUndefined();
|
|
@@ -128,7 +174,8 @@ describe("registerSubmitReportTool", () => {
|
|
|
128
174
|
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
129
175
|
tmpDirs.push(tmpDir);
|
|
130
176
|
const outputFile = path.join(tmpDir, "report.json");
|
|
131
|
-
const
|
|
177
|
+
const stateFile = await writeStateFile(tmpDir, []);
|
|
178
|
+
const result = await handler(sampleReportParams(outputFile, stateFile));
|
|
132
179
|
expect(result.isError).toBeUndefined();
|
|
133
180
|
const written = JSON.parse(await fs.readFile(outputFile, "utf-8"));
|
|
134
181
|
expect(written.commitMessage).toBe("Added recommendations by Skyramp Testbot.");
|
|
@@ -137,8 +184,9 @@ describe("registerSubmitReportTool", () => {
|
|
|
137
184
|
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
138
185
|
tmpDirs.push(tmpDir);
|
|
139
186
|
const outputFile = path.join(tmpDir, "report.json");
|
|
187
|
+
const stateFile = await writeStateFile(tmpDir, []);
|
|
140
188
|
const result = await handler({
|
|
141
|
-
...sampleReportParams(outputFile),
|
|
189
|
+
...sampleReportParams(outputFile, stateFile),
|
|
142
190
|
commitMessage: " line one\nline two\r\nline three ",
|
|
143
191
|
});
|
|
144
192
|
expect(result.isError).toBeUndefined();
|
|
@@ -150,9 +198,10 @@ describe("registerSubmitReportTool", () => {
|
|
|
150
198
|
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
151
199
|
tmpDirs.push(tmpDir);
|
|
152
200
|
const outputFile = path.join(tmpDir, "report.json");
|
|
201
|
+
const stateFile = await writeStateFile(tmpDir, []);
|
|
153
202
|
const longMessage = "add contract and integration tests for the new PATCH /orders/{order_id} endpoint including discount recalculation and line-item edits";
|
|
154
203
|
const result = await handler({
|
|
155
|
-
...sampleReportParams(outputFile),
|
|
204
|
+
...sampleReportParams(outputFile, stateFile),
|
|
156
205
|
commitMessage: longMessage,
|
|
157
206
|
});
|
|
158
207
|
expect(result.isError).toBeUndefined();
|
|
@@ -160,24 +209,13 @@ describe("registerSubmitReportTool", () => {
|
|
|
160
209
|
expect(written.commitMessage).toBe(longMessage);
|
|
161
210
|
expect(written.commitMessage.length).toBeGreaterThan(72);
|
|
162
211
|
});
|
|
163
|
-
it("should use default commitMessage when provided as empty string", async () => {
|
|
164
|
-
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
165
|
-
tmpDirs.push(tmpDir);
|
|
166
|
-
const outputFile = path.join(tmpDir, "report.json");
|
|
167
|
-
const result = await handler({
|
|
168
|
-
...sampleReportParams(outputFile),
|
|
169
|
-
commitMessage: "",
|
|
170
|
-
});
|
|
171
|
-
expect(result.isError).toBeUndefined();
|
|
172
|
-
const written = JSON.parse(await fs.readFile(outputFile, "utf-8"));
|
|
173
|
-
expect(written.commitMessage).toBe("Added recommendations by Skyramp Testbot.");
|
|
174
|
-
});
|
|
175
212
|
it("should use default commitMessage when whitespace-only", async () => {
|
|
176
213
|
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
177
214
|
tmpDirs.push(tmpDir);
|
|
178
215
|
const outputFile = path.join(tmpDir, "report.json");
|
|
216
|
+
const stateFile = await writeStateFile(tmpDir, []);
|
|
179
217
|
const result = await handler({
|
|
180
|
-
...sampleReportParams(outputFile),
|
|
218
|
+
...sampleReportParams(outputFile, stateFile),
|
|
181
219
|
commitMessage: " \n\r\n ",
|
|
182
220
|
});
|
|
183
221
|
expect(result.isError).toBeUndefined();
|
|
@@ -188,8 +226,9 @@ describe("registerSubmitReportTool", () => {
|
|
|
188
226
|
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
189
227
|
tmpDirs.push(tmpDir);
|
|
190
228
|
const outputFile = path.join(tmpDir, "report.json");
|
|
229
|
+
const stateFile = await writeStateFile(tmpDir, []);
|
|
191
230
|
const result = await handler({
|
|
192
|
-
...sampleReportParams(outputFile),
|
|
231
|
+
...sampleReportParams(outputFile, stateFile),
|
|
193
232
|
nextSteps: ["Check your targetSetupCommand — endpoints returned 404"],
|
|
194
233
|
});
|
|
195
234
|
expect(result.isError).toBeUndefined();
|
|
@@ -202,7 +241,8 @@ describe("registerSubmitReportTool", () => {
|
|
|
202
241
|
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
203
242
|
tmpDirs.push(tmpDir);
|
|
204
243
|
const outputFile = path.join(tmpDir, "report.json");
|
|
205
|
-
const
|
|
244
|
+
const stateFile = await writeStateFile(tmpDir, []);
|
|
245
|
+
const result = await handler(sampleReportParams(outputFile, stateFile));
|
|
206
246
|
expect(result.isError).toBeUndefined();
|
|
207
247
|
const written = JSON.parse(await fs.readFile(outputFile, "utf-8"));
|
|
208
248
|
expect(written.nextSteps).toEqual([]);
|
|
@@ -211,7 +251,8 @@ describe("registerSubmitReportTool", () => {
|
|
|
211
251
|
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
212
252
|
tmpDirs.push(tmpDir);
|
|
213
253
|
const outputFile = path.join(tmpDir, "report.json");
|
|
214
|
-
await
|
|
254
|
+
const stateFile = await writeStateFile(tmpDir, []);
|
|
255
|
+
await handler(sampleReportParams(outputFile, stateFile));
|
|
215
256
|
const raw = await fs.readFile(outputFile, "utf-8");
|
|
216
257
|
// Pretty-printed JSON should have indentation
|
|
217
258
|
expect(raw).toContain(" ");
|
|
@@ -222,8 +263,9 @@ describe("registerSubmitReportTool", () => {
|
|
|
222
263
|
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
223
264
|
tmpDirs.push(tmpDir);
|
|
224
265
|
const outputFile = path.join(tmpDir, "report.json");
|
|
266
|
+
const stateFile = await writeStateFile(tmpDir, []);
|
|
225
267
|
const params = {
|
|
226
|
-
...sampleReportParams(outputFile),
|
|
268
|
+
...sampleReportParams(outputFile, stateFile),
|
|
227
269
|
additionalRecommendations: [
|
|
228
270
|
{
|
|
229
271
|
testId: "integration-orders-patch-status",
|
|
@@ -516,14 +558,15 @@ describe("registerSubmitReportTool", () => {
|
|
|
516
558
|
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
517
559
|
tmpDirs.push(tmpDir);
|
|
518
560
|
const outputFile = path.join(tmpDir, "report.json");
|
|
519
|
-
// sampleReportParams: 1 new test, 1 maintenance (
|
|
561
|
+
// sampleReportParams: 1 new test, 1 maintenance (no matching stateFile entry), 2 results
|
|
520
562
|
// (1 Pass + 1 Fail), 1 issue (no severity).
|
|
521
|
-
await
|
|
563
|
+
const stateFile = await writeStateFile(tmpDir, []);
|
|
564
|
+
await handler(sampleReportParams(outputFile, stateFile));
|
|
522
565
|
const params = lastAnalyticsParams();
|
|
523
566
|
expect(params.testsGenerated).toBe("1");
|
|
524
567
|
expect(params.testsMaintained).toBe("1");
|
|
525
568
|
expect(params.recommendationsCount).toBe("0");
|
|
526
|
-
expect(params.maintenanceRecovered).toBe("
|
|
569
|
+
expect(params.maintenanceRecovered).toBe("0"); // no matching stateFile entry → no execution data
|
|
527
570
|
expect(params.testsPassed).toBe("1");
|
|
528
571
|
expect(params.testsFailed).toBe("1");
|
|
529
572
|
expect(params.testsSkipped).toBe("0");
|
|
@@ -533,8 +576,9 @@ describe("registerSubmitReportTool", () => {
|
|
|
533
576
|
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
534
577
|
tmpDirs.push(tmpDir);
|
|
535
578
|
const outputFile = path.join(tmpDir, "report.json");
|
|
579
|
+
const stateFile = await writeStateFile(tmpDir, []);
|
|
536
580
|
await handler({
|
|
537
|
-
...sampleReportParams(outputFile),
|
|
581
|
+
...sampleReportParams(outputFile, stateFile),
|
|
538
582
|
issuesFound: [
|
|
539
583
|
{ description: "Data corruption on checkout", severity: "critical" },
|
|
540
584
|
{ description: "Wrong total returned", severity: "high" },
|
|
@@ -554,8 +598,10 @@ describe("registerSubmitReportTool", () => {
|
|
|
554
598
|
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
555
599
|
tmpDirs.push(tmpDir);
|
|
556
600
|
const outputFile = path.join(tmpDir, "report.json");
|
|
601
|
+
const stateFile = await writeStateFile(tmpDir, []);
|
|
557
602
|
await handler({
|
|
558
603
|
summaryOutputFile: outputFile,
|
|
604
|
+
stateFile,
|
|
559
605
|
businessCaseAnalysis: "Config-only change.",
|
|
560
606
|
newTestsCreated: [],
|
|
561
607
|
testMaintenance: [],
|
|
@@ -573,36 +619,236 @@ describe("registerSubmitReportTool", () => {
|
|
|
573
619
|
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
574
620
|
tmpDirs.push(tmpDir);
|
|
575
621
|
const outputFile = path.join(tmpDir, "report.json");
|
|
622
|
+
const stateFile = await writeStateFile(tmpDir, []);
|
|
576
623
|
await handler({
|
|
577
|
-
...sampleReportParams(outputFile),
|
|
624
|
+
...sampleReportParams(outputFile, stateFile),
|
|
578
625
|
testMaintenance: [
|
|
579
626
|
// Real change
|
|
580
|
-
{ action: "UPDATE", testType: TestType.CONTRACT, endpoint: "GET /api/v1/products", fileName: "products_contract_test.py", description: "Patched auth",
|
|
581
|
-
{ action: "REGENERATE", testType: TestType.SMOKE, endpoint: "GET /api/v1/orders", fileName: "orders_smoke_test.py", description: "Rewrote for new shape",
|
|
627
|
+
{ action: "UPDATE", testType: TestType.CONTRACT, endpoint: "GET /api/v1/products", fileName: "products_contract_test.py", description: "Patched auth", beforeDetails: "", afterDetails: "" },
|
|
628
|
+
{ action: "REGENERATE", testType: TestType.SMOKE, endpoint: "GET /api/v1/orders", fileName: "orders_smoke_test.py", description: "Rewrote for new shape", beforeDetails: "", afterDetails: "" },
|
|
582
629
|
// No-ops — assessed but not modified
|
|
583
|
-
{ action: "IGNORE", testType: TestType.SMOKE, endpoint: "GET /api/v1/reviews", fileName: "reviews_smoke_test.py", description: "No action required",
|
|
584
|
-
{ action: "VERIFY", testType: TestType.SMOKE, endpoint: "GET /api/v1/coupons", fileName: "coupons_smoke_test.py", description: "Verified still valid",
|
|
630
|
+
{ action: "IGNORE", testType: TestType.SMOKE, endpoint: "GET /api/v1/reviews", fileName: "reviews_smoke_test.py", description: "No action required", beforeDetails: "", afterDetails: "" },
|
|
631
|
+
{ action: "VERIFY", testType: TestType.SMOKE, endpoint: "GET /api/v1/coupons", fileName: "coupons_smoke_test.py", description: "Verified still valid", beforeDetails: "", afterDetails: "" },
|
|
585
632
|
],
|
|
586
633
|
});
|
|
587
634
|
const params = lastAnalyticsParams();
|
|
588
635
|
// 2 change actions (UPDATE + REGENERATE), not 4
|
|
589
636
|
expect(params.testsMaintained).toBe("2");
|
|
590
|
-
//
|
|
591
|
-
expect(params.maintenanceRecovered).toBe("
|
|
637
|
+
// No matching stateFile entries — execution data unavailable
|
|
638
|
+
expect(params.maintenanceRecovered).toBe("0");
|
|
592
639
|
});
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
640
|
+
});
|
|
641
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
642
|
+
// testMaintenance output shapes
|
|
643
|
+
// Each case documents exactly what the MCP writes to JSON, which is what the
|
|
644
|
+
// testbot renderer reads. Keeping these in sync prevents silent regressions
|
|
645
|
+
// when toOutput or the stateFile enrichment path changes.
|
|
646
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
647
|
+
describe("testMaintenance output shapes", () => {
|
|
648
|
+
const baseMaintenance = {
|
|
649
|
+
testType: TestType.CONTRACT,
|
|
650
|
+
endpoint: "GET /api/v1/orders",
|
|
651
|
+
fileName: "orders_contract_test.py",
|
|
652
|
+
description: "Updated discount fields in assertions",
|
|
653
|
+
beforeDetails: "",
|
|
654
|
+
afterDetails: "",
|
|
655
|
+
};
|
|
656
|
+
async function writeAndRead(tmpDir, maintenance, extra) {
|
|
596
657
|
const outputFile = path.join(tmpDir, "report.json");
|
|
597
658
|
await handler({
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
],
|
|
659
|
+
summaryOutputFile: outputFile,
|
|
660
|
+
businessCaseAnalysis: "Test",
|
|
661
|
+
newTestsCreated: [],
|
|
662
|
+
testMaintenance: maintenance,
|
|
663
|
+
testResults: [],
|
|
664
|
+
issuesFound: [],
|
|
665
|
+
...extra,
|
|
603
666
|
});
|
|
604
|
-
const
|
|
605
|
-
|
|
667
|
+
const written = JSON.parse(await fs.readFile(outputFile, "utf-8"));
|
|
668
|
+
return written.testMaintenance;
|
|
669
|
+
}
|
|
670
|
+
// ── No matching stateFile entry ───────────────────────────────────────────
|
|
671
|
+
it("UPDATE with no matching stateFile entry: beforeStatus=Unknown, afterStatus=Unknown", async () => {
|
|
672
|
+
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
673
|
+
tmpDirs.push(tmpDir);
|
|
674
|
+
const stateFile = await writeStateFile(tmpDir, []);
|
|
675
|
+
const result = await writeAndRead(tmpDir, [{ ...baseMaintenance, action: DriftAction.Update }], { stateFile });
|
|
676
|
+
expect(result[0].beforeStatus).toBe("Unknown");
|
|
677
|
+
expect(result[0].afterStatus).toBe("Unknown");
|
|
678
|
+
expect(result[0].beforeDetails).toBe("");
|
|
679
|
+
expect(result[0].afterDetails).toBe("");
|
|
680
|
+
});
|
|
681
|
+
it("IGNORE with no matching stateFile entry: both Skipped", async () => {
|
|
682
|
+
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
683
|
+
tmpDirs.push(tmpDir);
|
|
684
|
+
const stateFile = await writeStateFile(tmpDir, []);
|
|
685
|
+
const result = await writeAndRead(tmpDir, [{ ...baseMaintenance, action: DriftAction.Ignore }], { stateFile });
|
|
686
|
+
expect(result[0].beforeStatus).toBe("Skipped");
|
|
687
|
+
expect(result[0].afterStatus).toBe("Skipped");
|
|
688
|
+
expect(result[0].beforeDetails).toBe("");
|
|
689
|
+
expect(result[0].afterDetails).toBe("");
|
|
690
|
+
});
|
|
691
|
+
it("DELETE with no matching stateFile entry: beforeStatus=Unknown, afterStatus=Skipped", async () => {
|
|
692
|
+
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
693
|
+
tmpDirs.push(tmpDir);
|
|
694
|
+
const stateFile = await writeStateFile(tmpDir, []);
|
|
695
|
+
const result = await writeAndRead(tmpDir, [{ ...baseMaintenance, action: DriftAction.Delete }], { stateFile });
|
|
696
|
+
expect(result[0].beforeStatus).toBe("Unknown");
|
|
697
|
+
expect(result[0].afterStatus).toBe("Skipped");
|
|
606
698
|
});
|
|
699
|
+
// ── With matching stateFile entry ─────────────────────────────────────────
|
|
700
|
+
it("UPDATE with before=Pass after=Pass: status from stateFile, details from LLM", async () => {
|
|
701
|
+
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
702
|
+
tmpDirs.push(tmpDir);
|
|
703
|
+
const stateFile = await writeStateFile(tmpDir, [{
|
|
704
|
+
testFile: "/repo/backend/tests/orders_contract_test.py",
|
|
705
|
+
executionBefore: { status: TestExecutionStatus.Pass, errors: [], duration: 15090 },
|
|
706
|
+
executionAfter: { status: TestExecutionStatus.Pass, errors: [], duration: 12880 },
|
|
707
|
+
}]);
|
|
708
|
+
const result = await writeAndRead(tmpDir, [{
|
|
709
|
+
...baseMaintenance, action: DriftAction.Update,
|
|
710
|
+
beforeDetails: "4 passed in 15.09s",
|
|
711
|
+
afterDetails: "5 passed in 12.88s",
|
|
712
|
+
}], { stateFile });
|
|
713
|
+
expect(result[0].beforeStatus).toBe("Pass");
|
|
714
|
+
expect(result[0].beforeDetails).toBe("4 passed in 15.09s");
|
|
715
|
+
expect(result[0].afterStatus).toBe("Pass");
|
|
716
|
+
expect(result[0].afterDetails).toBe("5 passed in 12.88s");
|
|
717
|
+
});
|
|
718
|
+
it("UPDATE with before=Pass after=Fail: status from stateFile, details from LLM", async () => {
|
|
719
|
+
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
720
|
+
tmpDirs.push(tmpDir);
|
|
721
|
+
const stateFile = await writeStateFile(tmpDir, [{
|
|
722
|
+
testFile: "/repo/backend/tests/orders_contract_test.py",
|
|
723
|
+
executionBefore: { status: TestExecutionStatus.Pass, errors: [], duration: 15090 },
|
|
724
|
+
executionAfter: { status: TestExecutionStatus.Fail, errors: [], duration: 10940 },
|
|
725
|
+
}]);
|
|
726
|
+
const result = await writeAndRead(tmpDir, [{
|
|
727
|
+
...baseMaintenance, action: DriftAction.Update,
|
|
728
|
+
beforeDetails: "4 passed in 15.09s",
|
|
729
|
+
afterDetails: "FAILED test_orders_get — check_schema fails, test isolation bug",
|
|
730
|
+
}], { stateFile });
|
|
731
|
+
expect(result[0].beforeStatus).toBe("Pass");
|
|
732
|
+
expect(result[0].afterStatus).toBe("Fail");
|
|
733
|
+
expect(result[0].afterDetails).toBe("FAILED test_orders_get — check_schema fails, test isolation bug");
|
|
734
|
+
});
|
|
735
|
+
it("UPDATE with crashed execution: status is Error", async () => {
|
|
736
|
+
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
737
|
+
tmpDirs.push(tmpDir);
|
|
738
|
+
const stateFile = await writeStateFile(tmpDir, [{
|
|
739
|
+
testFile: "/repo/backend/tests/orders_contract_test.py",
|
|
740
|
+
executionBefore: { status: TestExecutionStatus.Error, errors: ["Connection refused"], duration: 500 },
|
|
741
|
+
executionAfter: { status: TestExecutionStatus.Pass, errors: [], duration: 2000 },
|
|
742
|
+
}]);
|
|
743
|
+
const result = await writeAndRead(tmpDir, [{ ...baseMaintenance, action: DriftAction.Update }], { stateFile });
|
|
744
|
+
expect(result[0].beforeStatus).toBe("Error");
|
|
745
|
+
expect(result[0].afterStatus).toBe("Pass");
|
|
746
|
+
});
|
|
747
|
+
it("UPDATE Fail→Pass: counted as maintenanceRecovered in analytics", async () => {
|
|
748
|
+
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
749
|
+
tmpDirs.push(tmpDir);
|
|
750
|
+
const stateFile = await writeStateFile(tmpDir, [{
|
|
751
|
+
testFile: "/repo/backend/tests/orders_contract_test.py",
|
|
752
|
+
executionBefore: { status: TestExecutionStatus.Fail, errors: ["FAILED test_foo - assert False"], duration: 5000 },
|
|
753
|
+
executionAfter: { status: TestExecutionStatus.Pass, errors: [], duration: 5000 },
|
|
754
|
+
}]);
|
|
755
|
+
await writeAndRead(tmpDir, [{ ...baseMaintenance, action: DriftAction.Update }], { stateFile });
|
|
756
|
+
expect(pushMCPToolEventMock.mock.calls.at(-1)?.[2].maintenanceRecovered).toBe("1");
|
|
757
|
+
});
|
|
758
|
+
it("UPDATE with no before-execution recorded: beforeStatus=Unknown, afterStatus=Pass", async () => {
|
|
759
|
+
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
760
|
+
tmpDirs.push(tmpDir);
|
|
761
|
+
const stateFile = await writeStateFile(tmpDir, [{
|
|
762
|
+
testFile: "/repo/backend/tests/orders_contract_test.py",
|
|
763
|
+
// executionBefore absent — agent skipped before-phase
|
|
764
|
+
executionAfter: { status: TestExecutionStatus.Pass, errors: [], duration: 2000 },
|
|
765
|
+
}]);
|
|
766
|
+
const result = await writeAndRead(tmpDir, [{ ...baseMaintenance, action: DriftAction.Update }], { stateFile });
|
|
767
|
+
expect(result[0].beforeStatus).toBe("Unknown");
|
|
768
|
+
expect(result[0].afterStatus).toBe("Pass");
|
|
769
|
+
});
|
|
770
|
+
it("DELETE with before-execution, no after (file deleted): beforeStatus=Pass, afterStatus=Skipped", async () => {
|
|
771
|
+
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
772
|
+
tmpDirs.push(tmpDir);
|
|
773
|
+
const stateFile = await writeStateFile(tmpDir, [{
|
|
774
|
+
testFile: "/repo/backend/tests/orders_contract_test.py",
|
|
775
|
+
executionBefore: { status: TestExecutionStatus.Pass, errors: [], duration: 2000 },
|
|
776
|
+
// executionAfter absent — file was deleted, nothing to run
|
|
777
|
+
}]);
|
|
778
|
+
const result = await writeAndRead(tmpDir, [{ ...baseMaintenance, action: DriftAction.Delete }], { stateFile });
|
|
779
|
+
expect(result[0].beforeStatus).toBe("Pass");
|
|
780
|
+
expect(result[0].afterStatus).toBe("Skipped");
|
|
781
|
+
});
|
|
782
|
+
it("fileName matched by suffix: absolute stateFile path vs relative fileName", async () => {
|
|
783
|
+
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
784
|
+
tmpDirs.push(tmpDir);
|
|
785
|
+
const stateFile = await writeStateFile(tmpDir, [{
|
|
786
|
+
testFile: "/repo/backend/tests/python/orders_contract_test.py",
|
|
787
|
+
executionBefore: { status: TestExecutionStatus.Pass, errors: [], duration: 15000 },
|
|
788
|
+
executionAfter: { status: TestExecutionStatus.Pass, errors: [], duration: 12000 },
|
|
789
|
+
}]);
|
|
790
|
+
// fileName is relative — should still match via endsWith
|
|
791
|
+
const result = await writeAndRead(tmpDir, [{
|
|
792
|
+
...baseMaintenance,
|
|
793
|
+
action: DriftAction.Update,
|
|
794
|
+
fileName: "orders_contract_test.py",
|
|
795
|
+
}], { stateFile });
|
|
796
|
+
expect(result[0].beforeStatus).toBe("Pass");
|
|
797
|
+
expect(result[0].afterStatus).toBe("Pass");
|
|
798
|
+
});
|
|
799
|
+
});
|
|
800
|
+
it("testResult accepts videoPath field (optional)", async () => {
|
|
801
|
+
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
802
|
+
tmpDirs.push(tmpDir);
|
|
803
|
+
const outputFile = path.join(tmpDir, "report.json");
|
|
804
|
+
const stateFile = await writeStateFile(tmpDir, []);
|
|
805
|
+
const result = await handler({
|
|
806
|
+
summaryOutputFile: outputFile,
|
|
807
|
+
stateFile,
|
|
808
|
+
businessCaseAnalysis: "UI changes for login flow",
|
|
809
|
+
newTestsCreated: [],
|
|
810
|
+
testMaintenance: [],
|
|
811
|
+
testResults: [
|
|
812
|
+
{
|
|
813
|
+
testType: TestType.UI,
|
|
814
|
+
endpoint: "GET /login",
|
|
815
|
+
status: "Pass",
|
|
816
|
+
details: "5.2s, login_ui_test.spec.ts",
|
|
817
|
+
videoPath: "/workspace/.skyramp/videos/login_ui_test-abc12345/video.webm",
|
|
818
|
+
},
|
|
819
|
+
],
|
|
820
|
+
issuesFound: [],
|
|
821
|
+
});
|
|
822
|
+
expect(result.isError).toBeUndefined();
|
|
823
|
+
const written = JSON.parse(await fs.readFile(outputFile, "utf-8"));
|
|
824
|
+
expect(written.testResults).toHaveLength(1);
|
|
825
|
+
expect(written.testResults[0].videoPath).toBe("/workspace/.skyramp/videos/login_ui_test-abc12345/video.webm");
|
|
826
|
+
});
|
|
827
|
+
it("testResult accepts entry without videoPath (optional field)", async () => {
|
|
828
|
+
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
829
|
+
tmpDirs.push(tmpDir);
|
|
830
|
+
const outputFile = path.join(tmpDir, "report.json");
|
|
831
|
+
const stateFile = await writeStateFile(tmpDir, []);
|
|
832
|
+
const result = await handler({
|
|
833
|
+
summaryOutputFile: outputFile,
|
|
834
|
+
stateFile,
|
|
835
|
+
businessCaseAnalysis: "API endpoint changes",
|
|
836
|
+
newTestsCreated: [],
|
|
837
|
+
testMaintenance: [],
|
|
838
|
+
testResults: [
|
|
839
|
+
{
|
|
840
|
+
testType: TestType.CONTRACT,
|
|
841
|
+
endpoint: "POST /api/v1/products",
|
|
842
|
+
status: "Pass",
|
|
843
|
+
details: "2.1s, products_contract_test.py",
|
|
844
|
+
// videoPath intentionally omitted
|
|
845
|
+
},
|
|
846
|
+
],
|
|
847
|
+
issuesFound: [],
|
|
848
|
+
});
|
|
849
|
+
expect(result.isError).toBeUndefined();
|
|
850
|
+
const written = JSON.parse(await fs.readFile(outputFile, "utf-8"));
|
|
851
|
+
expect(written.testResults).toHaveLength(1);
|
|
852
|
+
expect(written.testResults[0].videoPath).toBeUndefined();
|
|
607
853
|
});
|
|
608
854
|
});
|
|
@@ -6,6 +6,7 @@ import * as fs from "fs";
|
|
|
6
6
|
import * as path from "path";
|
|
7
7
|
import { AnalyticsService } from "../../services/AnalyticsService.js";
|
|
8
8
|
import { toolError } from "../../utils/utils.js";
|
|
9
|
+
import { stageGeneratedPaths } from "../../utils/gitStaging.js";
|
|
9
10
|
import { buildRenameStrategy, buildFileRenameStrategy, buildUpdateStrategy, buildRegenerateStrategy, buildUpdateFileInstruction, buildRegenerateFileInstruction, } from "../../prompts/test-maintenance/actionsInstructions.js";
|
|
10
11
|
/**
|
|
11
12
|
* Compute a suggested new filename when an endpoint is renamed.
|
|
@@ -87,6 +88,10 @@ const actionsSchema = {
|
|
|
87
88
|
.string()
|
|
88
89
|
.refine((p) => path.isAbsolute(p), { message: "stateFile must be an absolute path" })
|
|
89
90
|
.describe("Path to state file from skyramp_analyze_changes"),
|
|
91
|
+
repository: z
|
|
92
|
+
.string()
|
|
93
|
+
.optional()
|
|
94
|
+
.describe("The owner/repo whose analysis section to act on (e.g. 'letsramp/api-insight'). In a multi-repo run, ALWAYS set it — the primary's owner/repo for the primary repo's tests, or a related repo's owner/repo for that repo's section — within the run-scoped stateFile. Omit only for a single-repo run."),
|
|
90
95
|
recommendations: z
|
|
91
96
|
.array(recommendationSchema)
|
|
92
97
|
.optional()
|
|
@@ -106,12 +111,12 @@ export function registerActionsTool(server) {
|
|
|
106
111
|
}, async (args) => {
|
|
107
112
|
let errorResult;
|
|
108
113
|
try {
|
|
109
|
-
logger.info(
|
|
110
|
-
// Load UnifiedAnalysisState
|
|
114
|
+
logger.info(`Performing Actions (unified test-management)${args.repository ? ` for repository ${args.repository}` : ""}`);
|
|
115
|
+
// Load UnifiedAnalysisState for the requested repo section (primary when
|
|
116
|
+
// repository omitted/matches the root) from the run-scoped state file.
|
|
111
117
|
const stateManager = StateManager.fromStatePath(args.stateFile);
|
|
112
|
-
const stateData = await stateManager.
|
|
113
|
-
const
|
|
114
|
-
const repositoryPath = fullState?.metadata.repositoryPath || "";
|
|
118
|
+
const stateData = await stateManager.readRepoData(args.repository);
|
|
119
|
+
const repositoryPath = (await stateManager.getRepoRepositoryPath(args.repository)) || "";
|
|
115
120
|
if (!stateData) {
|
|
116
121
|
errorResult = toolError(`State file is empty or invalid: ${args.stateFile}. Call skyramp_analyze_changes first to generate a valid state file.`);
|
|
117
122
|
return errorResult;
|
|
@@ -365,6 +370,15 @@ export function registerActionsTool(server) {
|
|
|
365
370
|
});
|
|
366
371
|
llmInstructionsObj.regenerate_strategy = buildRegenerateStrategy();
|
|
367
372
|
}
|
|
373
|
+
// Stage UPDATE and REGENERATE target paths so testbot's re-stage step
|
|
374
|
+
// commits the agent's edits even when files are outside testDirectory.
|
|
375
|
+
const stagedPaths = [
|
|
376
|
+
...updateRecommendations.map((r) => r.testFile),
|
|
377
|
+
...regenerateRecommendations.map((r) => r.testFile),
|
|
378
|
+
];
|
|
379
|
+
for (const p of stagedPaths) {
|
|
380
|
+
await stageGeneratedPaths(p).catch((e) => logger.warning(`Failed to stage maintained file ${p}: ${e.message}`));
|
|
381
|
+
}
|
|
368
382
|
const llmInstructions = JSON.stringify(llmInstructionsObj, null, 2);
|
|
369
383
|
const contentBlocks = [
|
|
370
384
|
{
|
|
@@ -394,8 +408,8 @@ export function registerActionsTool(server) {
|
|
|
394
408
|
finally {
|
|
395
409
|
let repositoryPath = "";
|
|
396
410
|
try {
|
|
397
|
-
|
|
398
|
-
|
|
411
|
+
repositoryPath =
|
|
412
|
+
(await StateManager.fromStatePath(args.stateFile).getRepoRepositoryPath(args.repository)) || "";
|
|
399
413
|
}
|
|
400
414
|
catch (analyticsError) {
|
|
401
415
|
logger.error(`Failed to read state for analytics in actions tool: ${analyticsError?.message ?? analyticsError}`, analyticsError);
|
|
@@ -18,7 +18,7 @@ export declare const analyzeChangesInputSchema: {
|
|
|
18
18
|
topN: z.ZodOptional<z.ZodDefault<z.ZodNumber>>;
|
|
19
19
|
maxGenerate: z.ZodOptional<z.ZodNumber>;
|
|
20
20
|
prNumber: z.ZodOptional<z.ZodNumber>;
|
|
21
|
-
|
|
21
|
+
repository: z.ZodOptional<z.ZodString>;
|
|
22
22
|
testsRepoDir: z.ZodOptional<z.ZodEffects<z.ZodString, string, string>>;
|
|
23
23
|
};
|
|
24
24
|
export declare const NO_UI_INSTRUCTIONS = "No UI changes detected \u2014 no blueprint capture needed.";
|