@skyramp/mcp 0.2.7 → 0.2.150-rc.mntnc
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/testbot/testbot-prompts.d.ts +1 -1
- package/build/prompts/testbot/testbot-prompts.js +23 -16
- package/build/prompts/testbot/testbot-prompts.test.js +0 -44
- package/build/resources/sutSetupResource.d.ts +2 -0
- package/build/resources/sutSetupResource.js +45 -0
- package/build/resources/testbotResource.js +1 -1
- package/build/services/TestDiscoveryService.d.ts +2 -2
- package/build/services/TestDiscoveryService.js +22 -12
- package/build/services/TestExecutionService.d.ts +9 -0
- package/build/services/TestExecutionService.js +158 -26
- package/build/services/TestExecutionService.test.js +306 -0
- package/build/tools/executeSkyrampTestTool.js +17 -4
- package/build/tools/generate-tests/generateBatchScenarioRestTool.js +1 -1
- package/build/tools/submitReportTool.js +81 -35
- package/build/tools/submitReportTool.test.js +264 -50
- package/build/tools/test-management/actionsTool.js +10 -0
- package/build/tools/test-management/analyzeChangesTool.d.ts +0 -1
- package/build/tools/test-management/analyzeChangesTool.js +84 -41
- package/build/tools/test-management/analyzeChangesTool.test.js +4 -34
- package/build/tools/test-management/analyzeTestHealthTool.js +6 -3
- 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.js +10 -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/node_modules/playwright/LICENSE +202 -0
- package/node_modules/playwright/NOTICE +5 -0
- package/node_modules/playwright/README.md +168 -0
- package/node_modules/playwright/lib/mcp/skyramp/traceRecordingBackend.js +13 -9
- package/node_modules/playwright/lib/mcp/test/skyRampExport.js +4 -3
- package/node_modules/playwright/node_modules/playwright-core/LICENSE +202 -0
- package/node_modules/playwright/node_modules/playwright-core/NOTICE +5 -0
- package/node_modules/playwright/node_modules/playwright-core/ThirdPartyNotices.txt +23 -126
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/assets/{codeMirrorModule-B0JOjboO.js → codeMirrorModule-B5kqh2EV.js} +1 -1
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/assets/{defaultSettingsView-1anWeyDf.js → defaultSettingsView-CZ9npQ3N.js} +79 -79
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/{index.Cc5029a3.js → index.BCnxj-_b.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.Wo5yvvVh.js → uiMode.1Ym0Ivn8.js} +1 -1
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/uiMode.html +2 -2
- package/node_modules/playwright/node_modules/playwright-core/package.json +1 -1
- package/node_modules/playwright/package.json +1 -1
- package/package.json +9 -2
- package/node_modules/playwright/node_modules/playwright-core/.DS_Store +0 -0
- 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/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"));
|
|
@@ -81,7 +89,8 @@ describe("registerSubmitReportTool", () => {
|
|
|
81
89
|
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
82
90
|
tmpDirs.push(tmpDir);
|
|
83
91
|
const outputFile = path.join(tmpDir, "nested", "dirs", "report.json");
|
|
84
|
-
const
|
|
92
|
+
const stateFile = await writeStateFile(tmpDir, []);
|
|
93
|
+
const result = await handler(sampleReportParams(outputFile, stateFile));
|
|
85
94
|
expect(result.isError).toBeUndefined();
|
|
86
95
|
const written = JSON.parse(await fs.readFile(outputFile, "utf-8"));
|
|
87
96
|
expect(written.businessCaseAnalysis).toBeDefined();
|
|
@@ -90,11 +99,12 @@ describe("registerSubmitReportTool", () => {
|
|
|
90
99
|
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
91
100
|
tmpDirs.push(tmpDir);
|
|
92
101
|
const outputFile = path.join(tmpDir, "report.json");
|
|
102
|
+
const stateFile = await writeStateFile(tmpDir, []);
|
|
93
103
|
const result = await handler({
|
|
94
104
|
summaryOutputFile: outputFile,
|
|
105
|
+
stateFile,
|
|
95
106
|
businessCaseAnalysis: "Simple config change.",
|
|
96
107
|
newTestsCreated: [],
|
|
97
|
-
testMaintenance: [],
|
|
98
108
|
testResults: [
|
|
99
109
|
{ testType: TestType.SMOKE, endpoint: "GET /api/v1/products", status: "Pass", details: "1.0s, test.py" },
|
|
100
110
|
],
|
|
@@ -103,12 +113,15 @@ describe("registerSubmitReportTool", () => {
|
|
|
103
113
|
expect(result.isError).toBeUndefined();
|
|
104
114
|
const written = JSON.parse(await fs.readFile(outputFile, "utf-8"));
|
|
105
115
|
expect(written.newTestsCreated).toEqual([]);
|
|
106
|
-
expect(written.testMaintenance).
|
|
116
|
+
expect(written.testMaintenance).toBeUndefined();
|
|
107
117
|
expect(written.issuesFound).toEqual([]);
|
|
108
118
|
expect(written.testResults).toHaveLength(1);
|
|
109
119
|
});
|
|
110
120
|
it("should return error for invalid file path", async () => {
|
|
111
|
-
const
|
|
121
|
+
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
122
|
+
tmpDirs.push(tmpDir);
|
|
123
|
+
const stateFile = await writeStateFile(tmpDir, []);
|
|
124
|
+
const result = await handler(sampleReportParams("/nonexistent/readonly/path/report.json", stateFile));
|
|
112
125
|
expect(result.isError).toBe(true);
|
|
113
126
|
expect(result.content[0].text).toContain("Failed to write report");
|
|
114
127
|
});
|
|
@@ -116,8 +129,9 @@ describe("registerSubmitReportTool", () => {
|
|
|
116
129
|
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
117
130
|
tmpDirs.push(tmpDir);
|
|
118
131
|
const outputFile = path.join(tmpDir, "report.json");
|
|
132
|
+
const stateFile = await writeStateFile(tmpDir, []);
|
|
119
133
|
const result = await handler({
|
|
120
|
-
...sampleReportParams(outputFile),
|
|
134
|
+
...sampleReportParams(outputFile, stateFile),
|
|
121
135
|
commitMessage: "add contract tests for /products endpoint",
|
|
122
136
|
});
|
|
123
137
|
expect(result.isError).toBeUndefined();
|
|
@@ -128,7 +142,8 @@ describe("registerSubmitReportTool", () => {
|
|
|
128
142
|
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
129
143
|
tmpDirs.push(tmpDir);
|
|
130
144
|
const outputFile = path.join(tmpDir, "report.json");
|
|
131
|
-
const
|
|
145
|
+
const stateFile = await writeStateFile(tmpDir, []);
|
|
146
|
+
const result = await handler(sampleReportParams(outputFile, stateFile));
|
|
132
147
|
expect(result.isError).toBeUndefined();
|
|
133
148
|
const written = JSON.parse(await fs.readFile(outputFile, "utf-8"));
|
|
134
149
|
expect(written.commitMessage).toBe("Added recommendations by Skyramp Testbot.");
|
|
@@ -137,8 +152,9 @@ describe("registerSubmitReportTool", () => {
|
|
|
137
152
|
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
138
153
|
tmpDirs.push(tmpDir);
|
|
139
154
|
const outputFile = path.join(tmpDir, "report.json");
|
|
155
|
+
const stateFile = await writeStateFile(tmpDir, []);
|
|
140
156
|
const result = await handler({
|
|
141
|
-
...sampleReportParams(outputFile),
|
|
157
|
+
...sampleReportParams(outputFile, stateFile),
|
|
142
158
|
commitMessage: " line one\nline two\r\nline three ",
|
|
143
159
|
});
|
|
144
160
|
expect(result.isError).toBeUndefined();
|
|
@@ -150,9 +166,10 @@ describe("registerSubmitReportTool", () => {
|
|
|
150
166
|
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
151
167
|
tmpDirs.push(tmpDir);
|
|
152
168
|
const outputFile = path.join(tmpDir, "report.json");
|
|
169
|
+
const stateFile = await writeStateFile(tmpDir, []);
|
|
153
170
|
const longMessage = "add contract and integration tests for the new PATCH /orders/{order_id} endpoint including discount recalculation and line-item edits";
|
|
154
171
|
const result = await handler({
|
|
155
|
-
...sampleReportParams(outputFile),
|
|
172
|
+
...sampleReportParams(outputFile, stateFile),
|
|
156
173
|
commitMessage: longMessage,
|
|
157
174
|
});
|
|
158
175
|
expect(result.isError).toBeUndefined();
|
|
@@ -160,24 +177,13 @@ describe("registerSubmitReportTool", () => {
|
|
|
160
177
|
expect(written.commitMessage).toBe(longMessage);
|
|
161
178
|
expect(written.commitMessage.length).toBeGreaterThan(72);
|
|
162
179
|
});
|
|
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
180
|
it("should use default commitMessage when whitespace-only", async () => {
|
|
176
181
|
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
177
182
|
tmpDirs.push(tmpDir);
|
|
178
183
|
const outputFile = path.join(tmpDir, "report.json");
|
|
184
|
+
const stateFile = await writeStateFile(tmpDir, []);
|
|
179
185
|
const result = await handler({
|
|
180
|
-
...sampleReportParams(outputFile),
|
|
186
|
+
...sampleReportParams(outputFile, stateFile),
|
|
181
187
|
commitMessage: " \n\r\n ",
|
|
182
188
|
});
|
|
183
189
|
expect(result.isError).toBeUndefined();
|
|
@@ -188,8 +194,9 @@ describe("registerSubmitReportTool", () => {
|
|
|
188
194
|
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
189
195
|
tmpDirs.push(tmpDir);
|
|
190
196
|
const outputFile = path.join(tmpDir, "report.json");
|
|
197
|
+
const stateFile = await writeStateFile(tmpDir, []);
|
|
191
198
|
const result = await handler({
|
|
192
|
-
...sampleReportParams(outputFile),
|
|
199
|
+
...sampleReportParams(outputFile, stateFile),
|
|
193
200
|
nextSteps: ["Check your targetSetupCommand — endpoints returned 404"],
|
|
194
201
|
});
|
|
195
202
|
expect(result.isError).toBeUndefined();
|
|
@@ -202,7 +209,8 @@ describe("registerSubmitReportTool", () => {
|
|
|
202
209
|
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
203
210
|
tmpDirs.push(tmpDir);
|
|
204
211
|
const outputFile = path.join(tmpDir, "report.json");
|
|
205
|
-
const
|
|
212
|
+
const stateFile = await writeStateFile(tmpDir, []);
|
|
213
|
+
const result = await handler(sampleReportParams(outputFile, stateFile));
|
|
206
214
|
expect(result.isError).toBeUndefined();
|
|
207
215
|
const written = JSON.parse(await fs.readFile(outputFile, "utf-8"));
|
|
208
216
|
expect(written.nextSteps).toEqual([]);
|
|
@@ -211,7 +219,8 @@ describe("registerSubmitReportTool", () => {
|
|
|
211
219
|
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
212
220
|
tmpDirs.push(tmpDir);
|
|
213
221
|
const outputFile = path.join(tmpDir, "report.json");
|
|
214
|
-
await
|
|
222
|
+
const stateFile = await writeStateFile(tmpDir, []);
|
|
223
|
+
await handler(sampleReportParams(outputFile, stateFile));
|
|
215
224
|
const raw = await fs.readFile(outputFile, "utf-8");
|
|
216
225
|
// Pretty-printed JSON should have indentation
|
|
217
226
|
expect(raw).toContain(" ");
|
|
@@ -222,8 +231,9 @@ describe("registerSubmitReportTool", () => {
|
|
|
222
231
|
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
223
232
|
tmpDirs.push(tmpDir);
|
|
224
233
|
const outputFile = path.join(tmpDir, "report.json");
|
|
234
|
+
const stateFile = await writeStateFile(tmpDir, []);
|
|
225
235
|
const params = {
|
|
226
|
-
...sampleReportParams(outputFile),
|
|
236
|
+
...sampleReportParams(outputFile, stateFile),
|
|
227
237
|
additionalRecommendations: [
|
|
228
238
|
{
|
|
229
239
|
testId: "integration-orders-patch-status",
|
|
@@ -516,14 +526,15 @@ describe("registerSubmitReportTool", () => {
|
|
|
516
526
|
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
517
527
|
tmpDirs.push(tmpDir);
|
|
518
528
|
const outputFile = path.join(tmpDir, "report.json");
|
|
519
|
-
// sampleReportParams: 1 new test, 1 maintenance (
|
|
529
|
+
// sampleReportParams: 1 new test, 1 maintenance (no matching stateFile entry), 2 results
|
|
520
530
|
// (1 Pass + 1 Fail), 1 issue (no severity).
|
|
521
|
-
await
|
|
531
|
+
const stateFile = await writeStateFile(tmpDir, []);
|
|
532
|
+
await handler(sampleReportParams(outputFile, stateFile));
|
|
522
533
|
const params = lastAnalyticsParams();
|
|
523
534
|
expect(params.testsGenerated).toBe("1");
|
|
524
535
|
expect(params.testsMaintained).toBe("1");
|
|
525
536
|
expect(params.recommendationsCount).toBe("0");
|
|
526
|
-
expect(params.maintenanceRecovered).toBe("
|
|
537
|
+
expect(params.maintenanceRecovered).toBe("0"); // no matching stateFile entry → no execution data
|
|
527
538
|
expect(params.testsPassed).toBe("1");
|
|
528
539
|
expect(params.testsFailed).toBe("1");
|
|
529
540
|
expect(params.testsSkipped).toBe("0");
|
|
@@ -533,8 +544,9 @@ describe("registerSubmitReportTool", () => {
|
|
|
533
544
|
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
534
545
|
tmpDirs.push(tmpDir);
|
|
535
546
|
const outputFile = path.join(tmpDir, "report.json");
|
|
547
|
+
const stateFile = await writeStateFile(tmpDir, []);
|
|
536
548
|
await handler({
|
|
537
|
-
...sampleReportParams(outputFile),
|
|
549
|
+
...sampleReportParams(outputFile, stateFile),
|
|
538
550
|
issuesFound: [
|
|
539
551
|
{ description: "Data corruption on checkout", severity: "critical" },
|
|
540
552
|
{ description: "Wrong total returned", severity: "high" },
|
|
@@ -554,8 +566,10 @@ describe("registerSubmitReportTool", () => {
|
|
|
554
566
|
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
555
567
|
tmpDirs.push(tmpDir);
|
|
556
568
|
const outputFile = path.join(tmpDir, "report.json");
|
|
569
|
+
const stateFile = await writeStateFile(tmpDir, []);
|
|
557
570
|
await handler({
|
|
558
571
|
summaryOutputFile: outputFile,
|
|
572
|
+
stateFile,
|
|
559
573
|
businessCaseAnalysis: "Config-only change.",
|
|
560
574
|
newTestsCreated: [],
|
|
561
575
|
testMaintenance: [],
|
|
@@ -573,36 +587,236 @@ describe("registerSubmitReportTool", () => {
|
|
|
573
587
|
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
574
588
|
tmpDirs.push(tmpDir);
|
|
575
589
|
const outputFile = path.join(tmpDir, "report.json");
|
|
590
|
+
const stateFile = await writeStateFile(tmpDir, []);
|
|
576
591
|
await handler({
|
|
577
|
-
...sampleReportParams(outputFile),
|
|
592
|
+
...sampleReportParams(outputFile, stateFile),
|
|
578
593
|
testMaintenance: [
|
|
579
594
|
// 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",
|
|
595
|
+
{ action: "UPDATE", testType: TestType.CONTRACT, endpoint: "GET /api/v1/products", fileName: "products_contract_test.py", description: "Patched auth", beforeDetails: "", afterDetails: "" },
|
|
596
|
+
{ action: "REGENERATE", testType: TestType.SMOKE, endpoint: "GET /api/v1/orders", fileName: "orders_smoke_test.py", description: "Rewrote for new shape", beforeDetails: "", afterDetails: "" },
|
|
582
597
|
// 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",
|
|
598
|
+
{ action: "IGNORE", testType: TestType.SMOKE, endpoint: "GET /api/v1/reviews", fileName: "reviews_smoke_test.py", description: "No action required", beforeDetails: "", afterDetails: "" },
|
|
599
|
+
{ action: "VERIFY", testType: TestType.SMOKE, endpoint: "GET /api/v1/coupons", fileName: "coupons_smoke_test.py", description: "Verified still valid", beforeDetails: "", afterDetails: "" },
|
|
585
600
|
],
|
|
586
601
|
});
|
|
587
602
|
const params = lastAnalyticsParams();
|
|
588
603
|
// 2 change actions (UPDATE + REGENERATE), not 4
|
|
589
604
|
expect(params.testsMaintained).toBe("2");
|
|
590
|
-
//
|
|
591
|
-
expect(params.maintenanceRecovered).toBe("
|
|
605
|
+
// No matching stateFile entries — execution data unavailable
|
|
606
|
+
expect(params.maintenanceRecovered).toBe("0");
|
|
592
607
|
});
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
608
|
+
});
|
|
609
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
610
|
+
// testMaintenance output shapes
|
|
611
|
+
// Each case documents exactly what the MCP writes to JSON, which is what the
|
|
612
|
+
// testbot renderer reads. Keeping these in sync prevents silent regressions
|
|
613
|
+
// when toOutput or the stateFile enrichment path changes.
|
|
614
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
615
|
+
describe("testMaintenance output shapes", () => {
|
|
616
|
+
const baseMaintenance = {
|
|
617
|
+
testType: TestType.CONTRACT,
|
|
618
|
+
endpoint: "GET /api/v1/orders",
|
|
619
|
+
fileName: "orders_contract_test.py",
|
|
620
|
+
description: "Updated discount fields in assertions",
|
|
621
|
+
beforeDetails: "",
|
|
622
|
+
afterDetails: "",
|
|
623
|
+
};
|
|
624
|
+
async function writeAndRead(tmpDir, maintenance, extra) {
|
|
596
625
|
const outputFile = path.join(tmpDir, "report.json");
|
|
597
626
|
await handler({
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
],
|
|
627
|
+
summaryOutputFile: outputFile,
|
|
628
|
+
businessCaseAnalysis: "Test",
|
|
629
|
+
newTestsCreated: [],
|
|
630
|
+
testMaintenance: maintenance,
|
|
631
|
+
testResults: [],
|
|
632
|
+
issuesFound: [],
|
|
633
|
+
...extra,
|
|
603
634
|
});
|
|
604
|
-
const
|
|
605
|
-
|
|
635
|
+
const written = JSON.parse(await fs.readFile(outputFile, "utf-8"));
|
|
636
|
+
return written.testMaintenance;
|
|
637
|
+
}
|
|
638
|
+
// ── No matching stateFile entry ───────────────────────────────────────────
|
|
639
|
+
it("UPDATE with no matching stateFile entry: beforeStatus=Unknown, afterStatus=Unknown", async () => {
|
|
640
|
+
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
641
|
+
tmpDirs.push(tmpDir);
|
|
642
|
+
const stateFile = await writeStateFile(tmpDir, []);
|
|
643
|
+
const result = await writeAndRead(tmpDir, [{ ...baseMaintenance, action: DriftAction.Update }], { stateFile });
|
|
644
|
+
expect(result[0].beforeStatus).toBe("Unknown");
|
|
645
|
+
expect(result[0].afterStatus).toBe("Unknown");
|
|
646
|
+
expect(result[0].beforeDetails).toBe("");
|
|
647
|
+
expect(result[0].afterDetails).toBe("");
|
|
648
|
+
});
|
|
649
|
+
it("IGNORE with no matching stateFile entry: both Skipped", async () => {
|
|
650
|
+
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
651
|
+
tmpDirs.push(tmpDir);
|
|
652
|
+
const stateFile = await writeStateFile(tmpDir, []);
|
|
653
|
+
const result = await writeAndRead(tmpDir, [{ ...baseMaintenance, action: DriftAction.Ignore }], { stateFile });
|
|
654
|
+
expect(result[0].beforeStatus).toBe("Skipped");
|
|
655
|
+
expect(result[0].afterStatus).toBe("Skipped");
|
|
656
|
+
expect(result[0].beforeDetails).toBe("");
|
|
657
|
+
expect(result[0].afterDetails).toBe("");
|
|
658
|
+
});
|
|
659
|
+
it("DELETE with no matching stateFile entry: beforeStatus=Unknown, afterStatus=Skipped", async () => {
|
|
660
|
+
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
661
|
+
tmpDirs.push(tmpDir);
|
|
662
|
+
const stateFile = await writeStateFile(tmpDir, []);
|
|
663
|
+
const result = await writeAndRead(tmpDir, [{ ...baseMaintenance, action: DriftAction.Delete }], { stateFile });
|
|
664
|
+
expect(result[0].beforeStatus).toBe("Unknown");
|
|
665
|
+
expect(result[0].afterStatus).toBe("Skipped");
|
|
606
666
|
});
|
|
667
|
+
// ── With matching stateFile entry ─────────────────────────────────────────
|
|
668
|
+
it("UPDATE with before=Pass after=Pass: status from stateFile, details from LLM", async () => {
|
|
669
|
+
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
670
|
+
tmpDirs.push(tmpDir);
|
|
671
|
+
const stateFile = await writeStateFile(tmpDir, [{
|
|
672
|
+
testFile: "/repo/backend/tests/orders_contract_test.py",
|
|
673
|
+
executionBefore: { status: TestExecutionStatus.Pass, errors: [], duration: 15090 },
|
|
674
|
+
executionAfter: { status: TestExecutionStatus.Pass, errors: [], duration: 12880 },
|
|
675
|
+
}]);
|
|
676
|
+
const result = await writeAndRead(tmpDir, [{
|
|
677
|
+
...baseMaintenance, action: DriftAction.Update,
|
|
678
|
+
beforeDetails: "4 passed in 15.09s",
|
|
679
|
+
afterDetails: "5 passed in 12.88s",
|
|
680
|
+
}], { stateFile });
|
|
681
|
+
expect(result[0].beforeStatus).toBe("Pass");
|
|
682
|
+
expect(result[0].beforeDetails).toBe("4 passed in 15.09s");
|
|
683
|
+
expect(result[0].afterStatus).toBe("Pass");
|
|
684
|
+
expect(result[0].afterDetails).toBe("5 passed in 12.88s");
|
|
685
|
+
});
|
|
686
|
+
it("UPDATE with before=Pass after=Fail: status from stateFile, details from LLM", async () => {
|
|
687
|
+
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
688
|
+
tmpDirs.push(tmpDir);
|
|
689
|
+
const stateFile = await writeStateFile(tmpDir, [{
|
|
690
|
+
testFile: "/repo/backend/tests/orders_contract_test.py",
|
|
691
|
+
executionBefore: { status: TestExecutionStatus.Pass, errors: [], duration: 15090 },
|
|
692
|
+
executionAfter: { status: TestExecutionStatus.Fail, errors: [], duration: 10940 },
|
|
693
|
+
}]);
|
|
694
|
+
const result = await writeAndRead(tmpDir, [{
|
|
695
|
+
...baseMaintenance, action: DriftAction.Update,
|
|
696
|
+
beforeDetails: "4 passed in 15.09s",
|
|
697
|
+
afterDetails: "FAILED test_orders_get — check_schema fails, test isolation bug",
|
|
698
|
+
}], { stateFile });
|
|
699
|
+
expect(result[0].beforeStatus).toBe("Pass");
|
|
700
|
+
expect(result[0].afterStatus).toBe("Fail");
|
|
701
|
+
expect(result[0].afterDetails).toBe("FAILED test_orders_get — check_schema fails, test isolation bug");
|
|
702
|
+
});
|
|
703
|
+
it("UPDATE with crashed execution: status is Error", async () => {
|
|
704
|
+
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
705
|
+
tmpDirs.push(tmpDir);
|
|
706
|
+
const stateFile = await writeStateFile(tmpDir, [{
|
|
707
|
+
testFile: "/repo/backend/tests/orders_contract_test.py",
|
|
708
|
+
executionBefore: { status: TestExecutionStatus.Error, errors: ["Connection refused"], duration: 500 },
|
|
709
|
+
executionAfter: { status: TestExecutionStatus.Pass, errors: [], duration: 2000 },
|
|
710
|
+
}]);
|
|
711
|
+
const result = await writeAndRead(tmpDir, [{ ...baseMaintenance, action: DriftAction.Update }], { stateFile });
|
|
712
|
+
expect(result[0].beforeStatus).toBe("Error");
|
|
713
|
+
expect(result[0].afterStatus).toBe("Pass");
|
|
714
|
+
});
|
|
715
|
+
it("UPDATE Fail→Pass: counted as maintenanceRecovered in analytics", async () => {
|
|
716
|
+
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
717
|
+
tmpDirs.push(tmpDir);
|
|
718
|
+
const stateFile = await writeStateFile(tmpDir, [{
|
|
719
|
+
testFile: "/repo/backend/tests/orders_contract_test.py",
|
|
720
|
+
executionBefore: { status: TestExecutionStatus.Fail, errors: ["FAILED test_foo - assert False"], duration: 5000 },
|
|
721
|
+
executionAfter: { status: TestExecutionStatus.Pass, errors: [], duration: 5000 },
|
|
722
|
+
}]);
|
|
723
|
+
await writeAndRead(tmpDir, [{ ...baseMaintenance, action: DriftAction.Update }], { stateFile });
|
|
724
|
+
expect(pushMCPToolEventMock.mock.calls.at(-1)?.[2].maintenanceRecovered).toBe("1");
|
|
725
|
+
});
|
|
726
|
+
it("UPDATE with no before-execution recorded: beforeStatus=Unknown, afterStatus=Pass", async () => {
|
|
727
|
+
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
728
|
+
tmpDirs.push(tmpDir);
|
|
729
|
+
const stateFile = await writeStateFile(tmpDir, [{
|
|
730
|
+
testFile: "/repo/backend/tests/orders_contract_test.py",
|
|
731
|
+
// executionBefore absent — agent skipped before-phase
|
|
732
|
+
executionAfter: { status: TestExecutionStatus.Pass, errors: [], duration: 2000 },
|
|
733
|
+
}]);
|
|
734
|
+
const result = await writeAndRead(tmpDir, [{ ...baseMaintenance, action: DriftAction.Update }], { stateFile });
|
|
735
|
+
expect(result[0].beforeStatus).toBe("Unknown");
|
|
736
|
+
expect(result[0].afterStatus).toBe("Pass");
|
|
737
|
+
});
|
|
738
|
+
it("DELETE with before-execution, no after (file deleted): beforeStatus=Pass, afterStatus=Skipped", async () => {
|
|
739
|
+
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
740
|
+
tmpDirs.push(tmpDir);
|
|
741
|
+
const stateFile = await writeStateFile(tmpDir, [{
|
|
742
|
+
testFile: "/repo/backend/tests/orders_contract_test.py",
|
|
743
|
+
executionBefore: { status: TestExecutionStatus.Pass, errors: [], duration: 2000 },
|
|
744
|
+
// executionAfter absent — file was deleted, nothing to run
|
|
745
|
+
}]);
|
|
746
|
+
const result = await writeAndRead(tmpDir, [{ ...baseMaintenance, action: DriftAction.Delete }], { stateFile });
|
|
747
|
+
expect(result[0].beforeStatus).toBe("Pass");
|
|
748
|
+
expect(result[0].afterStatus).toBe("Skipped");
|
|
749
|
+
});
|
|
750
|
+
it("fileName matched by suffix: absolute stateFile path vs relative fileName", async () => {
|
|
751
|
+
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
752
|
+
tmpDirs.push(tmpDir);
|
|
753
|
+
const stateFile = await writeStateFile(tmpDir, [{
|
|
754
|
+
testFile: "/repo/backend/tests/python/orders_contract_test.py",
|
|
755
|
+
executionBefore: { status: TestExecutionStatus.Pass, errors: [], duration: 15000 },
|
|
756
|
+
executionAfter: { status: TestExecutionStatus.Pass, errors: [], duration: 12000 },
|
|
757
|
+
}]);
|
|
758
|
+
// fileName is relative — should still match via endsWith
|
|
759
|
+
const result = await writeAndRead(tmpDir, [{
|
|
760
|
+
...baseMaintenance,
|
|
761
|
+
action: DriftAction.Update,
|
|
762
|
+
fileName: "orders_contract_test.py",
|
|
763
|
+
}], { stateFile });
|
|
764
|
+
expect(result[0].beforeStatus).toBe("Pass");
|
|
765
|
+
expect(result[0].afterStatus).toBe("Pass");
|
|
766
|
+
});
|
|
767
|
+
});
|
|
768
|
+
it("testResult accepts videoPath field (optional)", async () => {
|
|
769
|
+
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
770
|
+
tmpDirs.push(tmpDir);
|
|
771
|
+
const outputFile = path.join(tmpDir, "report.json");
|
|
772
|
+
const stateFile = await writeStateFile(tmpDir, []);
|
|
773
|
+
const result = await handler({
|
|
774
|
+
summaryOutputFile: outputFile,
|
|
775
|
+
stateFile,
|
|
776
|
+
businessCaseAnalysis: "UI changes for login flow",
|
|
777
|
+
newTestsCreated: [],
|
|
778
|
+
testMaintenance: [],
|
|
779
|
+
testResults: [
|
|
780
|
+
{
|
|
781
|
+
testType: TestType.UI,
|
|
782
|
+
endpoint: "GET /login",
|
|
783
|
+
status: "Pass",
|
|
784
|
+
details: "5.2s, login_ui_test.spec.ts",
|
|
785
|
+
videoPath: "/workspace/.skyramp/videos/login_ui_test-abc12345/video.webm",
|
|
786
|
+
},
|
|
787
|
+
],
|
|
788
|
+
issuesFound: [],
|
|
789
|
+
});
|
|
790
|
+
expect(result.isError).toBeUndefined();
|
|
791
|
+
const written = JSON.parse(await fs.readFile(outputFile, "utf-8"));
|
|
792
|
+
expect(written.testResults).toHaveLength(1);
|
|
793
|
+
expect(written.testResults[0].videoPath).toBe("/workspace/.skyramp/videos/login_ui_test-abc12345/video.webm");
|
|
794
|
+
});
|
|
795
|
+
it("testResult accepts entry without videoPath (optional field)", async () => {
|
|
796
|
+
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
797
|
+
tmpDirs.push(tmpDir);
|
|
798
|
+
const outputFile = path.join(tmpDir, "report.json");
|
|
799
|
+
const stateFile = await writeStateFile(tmpDir, []);
|
|
800
|
+
const result = await handler({
|
|
801
|
+
summaryOutputFile: outputFile,
|
|
802
|
+
stateFile,
|
|
803
|
+
businessCaseAnalysis: "API endpoint changes",
|
|
804
|
+
newTestsCreated: [],
|
|
805
|
+
testMaintenance: [],
|
|
806
|
+
testResults: [
|
|
807
|
+
{
|
|
808
|
+
testType: TestType.CONTRACT,
|
|
809
|
+
endpoint: "POST /api/v1/products",
|
|
810
|
+
status: "Pass",
|
|
811
|
+
details: "2.1s, products_contract_test.py",
|
|
812
|
+
// videoPath intentionally omitted
|
|
813
|
+
},
|
|
814
|
+
],
|
|
815
|
+
issuesFound: [],
|
|
816
|
+
});
|
|
817
|
+
expect(result.isError).toBeUndefined();
|
|
818
|
+
const written = JSON.parse(await fs.readFile(outputFile, "utf-8"));
|
|
819
|
+
expect(written.testResults).toHaveLength(1);
|
|
820
|
+
expect(written.testResults[0].videoPath).toBeUndefined();
|
|
607
821
|
});
|
|
608
822
|
});
|
|
@@ -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.
|
|
@@ -365,6 +366,15 @@ export function registerActionsTool(server) {
|
|
|
365
366
|
});
|
|
366
367
|
llmInstructionsObj.regenerate_strategy = buildRegenerateStrategy();
|
|
367
368
|
}
|
|
369
|
+
// Stage UPDATE and REGENERATE target paths so testbot's re-stage step
|
|
370
|
+
// commits the agent's edits even when files are outside testDirectory.
|
|
371
|
+
const stagedPaths = [
|
|
372
|
+
...updateRecommendations.map((r) => r.testFile),
|
|
373
|
+
...regenerateRecommendations.map((r) => r.testFile),
|
|
374
|
+
];
|
|
375
|
+
for (const p of stagedPaths) {
|
|
376
|
+
await stageGeneratedPaths(p).catch((e) => logger.warning(`Failed to stage maintained file ${p}: ${e.message}`));
|
|
377
|
+
}
|
|
368
378
|
const llmInstructions = JSON.stringify(llmInstructionsObj, null, 2);
|
|
369
379
|
const contentBlocks = [
|
|
370
380
|
{
|
|
@@ -18,7 +18,6 @@ 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
|
-
stateOutputFile: z.ZodOptional<z.ZodEffects<z.ZodString, string, string>>;
|
|
22
21
|
testsRepoDir: z.ZodOptional<z.ZodEffects<z.ZodString, string, string>>;
|
|
23
22
|
};
|
|
24
23
|
export declare const NO_UI_INSTRUCTIONS = "No UI changes detected \u2014 no blueprint capture needed.";
|