@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.
Files changed (67) hide show
  1. package/build/index.js +5 -2
  2. package/build/prompts/enhance-assertions/uiAssertionsPrompt.js +24 -5
  3. package/build/prompts/sut-setup/modes/adaptWorkflowPrompt.d.ts +2 -0
  4. package/build/prompts/sut-setup/modes/adaptWorkflowPrompt.js +114 -0
  5. package/build/prompts/sut-setup/modes/dockerComposePrompt.d.ts +2 -0
  6. package/build/prompts/sut-setup/modes/dockerComposePrompt.js +292 -0
  7. package/build/prompts/sut-setup/shared.d.ts +39 -0
  8. package/build/prompts/sut-setup/shared.js +132 -0
  9. package/build/prompts/test-maintenance/driftAnalysisSections.js +9 -3
  10. package/build/prompts/test-recommendation/analysisOutputPrompt.js +1 -1
  11. package/build/prompts/testbot/testbot-prompts.d.ts +1 -1
  12. package/build/prompts/testbot/testbot-prompts.js +23 -16
  13. package/build/prompts/testbot/testbot-prompts.test.js +0 -44
  14. package/build/resources/sutSetupResource.d.ts +2 -0
  15. package/build/resources/sutSetupResource.js +45 -0
  16. package/build/resources/testbotResource.js +1 -1
  17. package/build/services/TestDiscoveryService.d.ts +2 -2
  18. package/build/services/TestDiscoveryService.js +22 -12
  19. package/build/services/TestExecutionService.d.ts +9 -0
  20. package/build/services/TestExecutionService.js +158 -26
  21. package/build/services/TestExecutionService.test.js +306 -0
  22. package/build/tools/executeSkyrampTestTool.js +17 -4
  23. package/build/tools/generate-tests/generateBatchScenarioRestTool.js +1 -1
  24. package/build/tools/submitReportTool.js +81 -35
  25. package/build/tools/submitReportTool.test.js +264 -50
  26. package/build/tools/test-management/actionsTool.js +10 -0
  27. package/build/tools/test-management/analyzeChangesTool.d.ts +0 -1
  28. package/build/tools/test-management/analyzeChangesTool.js +84 -41
  29. package/build/tools/test-management/analyzeChangesTool.test.js +4 -34
  30. package/build/tools/test-management/analyzeTestHealthTool.js +6 -3
  31. package/build/types/TestAnalysis.d.ts +2 -1
  32. package/build/types/TestExecution.d.ts +12 -2
  33. package/build/types/TestExecution.js +11 -1
  34. package/build/types/TestbotReport.d.ts +68 -0
  35. package/build/types/TestbotReport.js +1 -0
  36. package/build/types/index.d.ts +4 -0
  37. package/build/types/index.js +3 -0
  38. package/build/utils/AnalysisStateManager.js +10 -1
  39. package/build/utils/routeParsers.d.ts +0 -10
  40. package/build/utils/routeParsers.js +31 -38
  41. package/build/utils/scenarioDrafting.d.ts +1 -1
  42. package/build/utils/scenarioDrafting.js +23 -1
  43. package/build/utils/utils.d.ts +7 -1
  44. package/build/utils/utils.js +10 -0
  45. package/node_modules/playwright/LICENSE +202 -0
  46. package/node_modules/playwright/NOTICE +5 -0
  47. package/node_modules/playwright/README.md +168 -0
  48. package/node_modules/playwright/lib/mcp/skyramp/traceRecordingBackend.js +13 -9
  49. package/node_modules/playwright/lib/mcp/test/skyRampExport.js +4 -3
  50. package/node_modules/playwright/node_modules/playwright-core/LICENSE +202 -0
  51. package/node_modules/playwright/node_modules/playwright-core/NOTICE +5 -0
  52. package/node_modules/playwright/node_modules/playwright-core/ThirdPartyNotices.txt +23 -126
  53. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/assets/{codeMirrorModule-B0JOjboO.js → codeMirrorModule-B5kqh2EV.js} +1 -1
  54. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/assets/{defaultSettingsView-1anWeyDf.js → defaultSettingsView-CZ9npQ3N.js} +79 -79
  55. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/{index.Cc5029a3.js → index.BCnxj-_b.js} +1 -1
  56. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/index.html +2 -2
  57. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/{uiMode.Wo5yvvVh.js → uiMode.1Ym0Ivn8.js} +1 -1
  58. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/uiMode.html +2 -2
  59. package/node_modules/playwright/node_modules/playwright-core/package.json +1 -1
  60. package/node_modules/playwright/package.json +1 -1
  61. package/package.json +9 -2
  62. package/node_modules/playwright/node_modules/playwright-core/.DS_Store +0 -0
  63. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/.bin/node-which +0 -52
  64. package/node_modules/playwright/node_modules/playwright-core/bundles/utils/node_modules/.bin/is-docker +0 -5
  65. package/node_modules/playwright/node_modules/playwright-core/bundles/utils/node_modules/.bin/mime +0 -46
  66. package/node_modules/playwright/node_modules/playwright-core/bundles/utils/node_modules/.bin/yaml +0 -11
  67. 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 sampleReportParams(outputFile) {
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", beforeStatus: "Fail", beforeDetails: "401 Unauthorized (1.5s)", afterStatus: "Pass", afterDetails: "All assertions passed (2.3s)" }],
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 result = await handler(sampleReportParams(outputFile));
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 result = await handler(sampleReportParams(outputFile));
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).toEqual([]);
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 result = await handler(sampleReportParams("/nonexistent/readonly/path/report.json"));
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 result = await handler(sampleReportParams(outputFile));
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 result = await handler(sampleReportParams(outputFile));
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 handler(sampleReportParams(outputFile));
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 (Fail→Pass), 2 results
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 handler(sampleReportParams(outputFile));
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("1");
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", beforeStatus: "Fail", beforeDetails: "401 (1.5s)", afterStatus: "Pass", afterDetails: "passed (2.3s)" },
581
- { action: "REGENERATE", testType: TestType.SMOKE, endpoint: "GET /api/v1/orders", fileName: "orders_smoke_test.py", description: "Rewrote for new shape", beforeStatus: "Fail", beforeDetails: "shape mismatch", afterStatus: "Pass", afterDetails: "passed (1.0s)" },
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", beforeStatus: "Pass", beforeDetails: "drift score 0", afterStatus: "Skipped", afterDetails: "IGNORE: endpoint not in PR" },
584
- { action: "VERIFY", testType: TestType.SMOKE, endpoint: "GET /api/v1/coupons", fileName: "coupons_smoke_test.py", description: "Verified still valid", beforeStatus: "Pass", beforeDetails: "minor drift", afterStatus: "Skipped", afterDetails: "VERIFY: assertions still hold" },
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
- // Both changes went Fail→Pass
591
- expect(params.maintenanceRecovered).toBe("2");
605
+ // No matching stateFile entries — execution data unavailable
606
+ expect(params.maintenanceRecovered).toBe("0");
592
607
  });
593
- it("counts UPDATE/REGENERATE/DELETE as maintenance changes, not VERIFY/IGNORE", async () => {
594
- const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
595
- tmpDirs.push(tmpDir);
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
- ...sampleReportParams(outputFile),
599
- testMaintenance: [
600
- { testType: TestType.CONTRACT, endpoint: "GET /api/v1/products", fileName: "products_contract_test.py", action: DriftAction.Update, description: "Patched", beforeStatus: "Fail", beforeDetails: "401", afterStatus: "Pass", afterDetails: "passed" },
601
- { testType: TestType.SMOKE, endpoint: "GET /api/v1/reviews", fileName: "reviews_smoke_test.py", action: DriftAction.Ignore, description: "No action required", beforeStatus: "Pass", beforeDetails: "drift 0", afterStatus: "Skipped", afterDetails: "not in PR" },
602
- ],
627
+ summaryOutputFile: outputFile,
628
+ businessCaseAnalysis: "Test",
629
+ newTestsCreated: [],
630
+ testMaintenance: maintenance,
631
+ testResults: [],
632
+ issuesFound: [],
633
+ ...extra,
603
634
  });
604
- const params = lastAnalyticsParams();
605
- expect(params.testsMaintained).toBe("1");
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.";