@skyramp/mcp 0.2.6 → 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.
Files changed (248) hide show
  1. package/build/commands/commandLibrary.d.ts +12 -0
  2. package/build/commands/recommendTestsAndExecuteCommand.d.ts +2 -0
  3. package/build/commands/testThisEndpointCommand.d.ts +2 -0
  4. package/build/index.d.ts +2 -0
  5. package/build/index.js +6 -4
  6. package/build/playwright/PlaywrightTraceService.d.ts +32 -0
  7. package/build/playwright/index.d.ts +3 -0
  8. package/build/playwright/registerPlaywrightTools.d.ts +8 -0
  9. package/build/playwright/registerPlaywrightTools.js +12 -11
  10. package/build/playwright/traceRecordingPrompt.d.ts +13 -0
  11. package/build/playwright/traceRecordingPrompt.js +3 -11
  12. package/build/prompts/code-reuse.d.ts +1 -0
  13. package/build/prompts/enhance-assertions/contractProviderAssertionsPrompt.d.ts +2 -0
  14. package/build/prompts/enhance-assertions/integrationAssertionsPrompt.d.ts +2 -0
  15. package/build/prompts/enhance-assertions/sharedAssertionRules.d.ts +15 -0
  16. package/build/prompts/enhance-assertions/uiAssertionsPrompt.d.ts +2 -0
  17. package/build/prompts/enhance-assertions/uiAssertionsPrompt.js +24 -5
  18. package/build/prompts/fix-error-prompt.d.ts +1 -0
  19. package/build/prompts/initialize-workspace/initializeWorkspacePrompt.d.ts +1 -0
  20. package/build/prompts/modularization/integration-test-modularization.d.ts +1 -0
  21. package/build/prompts/modularization/ui-test-modularization.d.ts +1 -0
  22. package/build/prompts/personas.d.ts +21 -0
  23. package/build/prompts/pom-aware-code-reuse.d.ts +1 -0
  24. package/build/prompts/startTraceCollectionPrompts.d.ts +2 -0
  25. package/build/prompts/sut-setup/modes/adaptWorkflowPrompt.d.ts +2 -0
  26. package/build/prompts/sut-setup/modes/adaptWorkflowPrompt.js +114 -0
  27. package/build/prompts/sut-setup/modes/dockerComposePrompt.d.ts +2 -0
  28. package/build/prompts/sut-setup/modes/dockerComposePrompt.js +292 -0
  29. package/build/prompts/sut-setup/shared.d.ts +39 -0
  30. package/build/prompts/sut-setup/shared.js +132 -0
  31. package/build/prompts/test-maintenance/actionsInstructions.d.ts +35 -0
  32. package/build/prompts/test-maintenance/drift-analysis-prompt.d.ts +4 -0
  33. package/build/prompts/test-maintenance/driftAnalysisSections.d.ts +13 -0
  34. package/build/prompts/test-maintenance/driftAnalysisSections.js +9 -3
  35. package/build/prompts/test-recommendation/analysisOutputPrompt.d.ts +84 -0
  36. package/build/prompts/test-recommendation/analysisOutputPrompt.js +1 -1
  37. package/build/prompts/test-recommendation/analysisOutputPrompt.test.d.ts +1 -0
  38. package/build/prompts/test-recommendation/analysisOutputPrompt.test.js +1 -0
  39. package/build/prompts/test-recommendation/diffExecutionPlan.d.ts +17 -0
  40. package/build/prompts/test-recommendation/diffExecutionPlan.js +83 -18
  41. package/build/prompts/test-recommendation/fullRepoCatalog.d.ts +7 -0
  42. package/build/prompts/test-recommendation/mergeEnrichedScenarios.test.d.ts +1 -0
  43. package/build/prompts/test-recommendation/mergeEnrichedScenarios.test.js +1 -0
  44. package/build/prompts/test-recommendation/promptPlan.d.ts +203 -0
  45. package/build/prompts/test-recommendation/promptPlan.test.d.ts +1 -0
  46. package/build/prompts/test-recommendation/recommendationSections.d.ts +35 -0
  47. package/build/prompts/test-recommendation/recommendationShared.d.ts +31 -0
  48. package/build/prompts/test-recommendation/registerRecommendTestsPrompt.d.ts +7 -0
  49. package/build/prompts/test-recommendation/scopeAssessment.d.ts +55 -0
  50. package/build/prompts/test-recommendation/scopeAssessment.js +2 -2
  51. package/build/prompts/test-recommendation/scopeAssessment.test.d.ts +1 -0
  52. package/build/prompts/test-recommendation/scopeAssessment.test.js +1 -0
  53. package/build/prompts/test-recommendation/test-recommendation-prompt.d.ts +6 -0
  54. package/build/prompts/test-recommendation/test-recommendation-prompt.js +4 -4
  55. package/build/prompts/test-recommendation/test-recommendation-prompt.test.d.ts +1 -0
  56. package/build/prompts/test-recommendation/test-recommendation-prompt.test.js +97 -99
  57. package/build/prompts/testbot/testbot-prompts.d.ts +26 -0
  58. package/build/prompts/testbot/testbot-prompts.js +301 -46
  59. package/build/prompts/testbot/testbot-prompts.test.d.ts +1 -0
  60. package/build/prompts/testbot/testbot-prompts.test.js +109 -117
  61. package/build/resources/analysisResources.d.ts +17 -0
  62. package/build/resources/progressResource.d.ts +2 -0
  63. package/build/resources/sutSetupResource.d.ts +2 -0
  64. package/build/resources/sutSetupResource.js +45 -0
  65. package/build/resources/testbotResource.d.ts +2 -0
  66. package/build/resources/testbotResource.js +2 -2
  67. package/build/services/AnalyticsService.d.ts +13 -0
  68. package/build/services/AnalyticsService.test.d.ts +1 -0
  69. package/build/services/AnalyticsService.test.js +86 -0
  70. package/build/services/ModularizationService.d.ts +11 -0
  71. package/build/services/ScenarioGenerationService.d.ts +37 -0
  72. package/build/services/ScenarioGenerationService.integration.test.d.ts +1 -0
  73. package/build/services/ScenarioGenerationService.integration.test.js +4 -0
  74. package/build/services/ScenarioGenerationService.test.d.ts +1 -0
  75. package/build/services/TestDiscoveryService.d.ts +128 -0
  76. package/build/services/TestDiscoveryService.js +22 -12
  77. package/build/services/TestDiscoveryService.test.d.ts +1 -0
  78. package/build/services/TestDiscoveryService.test.js +4 -3
  79. package/build/services/TestExecutionService.d.ts +55 -0
  80. package/build/services/TestExecutionService.js +158 -26
  81. package/build/services/TestExecutionService.test.d.ts +1 -0
  82. package/build/services/TestExecutionService.test.js +339 -25
  83. package/build/services/TestGenerationService.d.ts +56 -0
  84. package/build/services/TestGenerationService.test.d.ts +1 -0
  85. package/build/services/TestGenerationService.test.js +10 -8
  86. package/build/services/containerEnv.d.ts +14 -0
  87. package/build/tool-phase-coverage.test.d.ts +1 -0
  88. package/build/tool-phase-coverage.test.js +2 -0
  89. package/build/tool-phases.d.ts +38 -0
  90. package/build/tools/auth/loginTool.d.ts +2 -0
  91. package/build/tools/auth/logoutTool.d.ts +2 -0
  92. package/build/tools/code-refactor/codeReuseTool.d.ts +2 -0
  93. package/build/tools/code-refactor/enhanceAssertionsTool.d.ts +2 -0
  94. package/build/tools/code-refactor/modularizationTool.d.ts +2 -0
  95. package/build/tools/executeSkyrampTestTool.d.ts +2 -0
  96. package/build/tools/executeSkyrampTestTool.js +26 -7
  97. package/build/tools/fixErrorTool.d.ts +2 -0
  98. package/build/tools/generate-tests/generateBatchScenarioRestTool.d.ts +45 -0
  99. package/build/tools/generate-tests/generateBatchScenarioRestTool.js +1 -1
  100. package/build/tools/generate-tests/generateBatchScenarioRestTool.test.d.ts +1 -0
  101. package/build/tools/generate-tests/generateBatchScenarioRestTool.test.js +11 -9
  102. package/build/tools/generate-tests/generateContractRestTool.d.ts +31 -0
  103. package/build/tools/generate-tests/generateE2ERestTool.d.ts +13 -0
  104. package/build/tools/generate-tests/generateFuzzRestTool.d.ts +10 -0
  105. package/build/tools/generate-tests/generateIntegrationRestTool.d.ts +16 -0
  106. package/build/tools/generate-tests/generateLoadRestTool.d.ts +18 -0
  107. package/build/tools/generate-tests/generateLoadRestTool.test.d.ts +1 -0
  108. package/build/tools/generate-tests/generateMockRestTool.d.ts +64 -0
  109. package/build/tools/generate-tests/generateSmokeRestTool.d.ts +8 -0
  110. package/build/tools/generate-tests/generateUIRestTool.d.ts +14 -0
  111. package/build/tools/generate-tests/generateUIRestTool.js +1 -1
  112. package/build/tools/generate-tests/loadTestSchema.d.ts +39 -0
  113. package/build/tools/one-click/oneClickTool.d.ts +11 -0
  114. package/build/tools/submitReportTool.d.ts +394 -0
  115. package/build/tools/submitReportTool.js +250 -41
  116. package/build/tools/submitReportTool.test.d.ts +1 -0
  117. package/build/tools/submitReportTool.test.js +511 -55
  118. package/build/tools/test-management/actionsTool.d.ts +10 -0
  119. package/build/tools/test-management/actionsTool.js +21 -7
  120. package/build/tools/test-management/analyzeChangesTool.d.ts +27 -0
  121. package/build/tools/test-management/analyzeChangesTool.js +150 -48
  122. package/build/tools/test-management/analyzeChangesTool.test.d.ts +1 -0
  123. package/build/tools/test-management/analyzeChangesTool.test.js +95 -78
  124. package/build/tools/test-management/analyzeTestHealthTool.d.ts +2 -0
  125. package/build/tools/test-management/analyzeTestHealthTool.js +16 -9
  126. package/build/tools/test-management/index.d.ts +3 -0
  127. package/build/tools/test-management/index.js +0 -1
  128. package/build/tools/trace/resolveSaveStoragePath.d.ts +7 -0
  129. package/build/tools/trace/resolveSaveStoragePath.test.d.ts +1 -0
  130. package/build/tools/trace/resolveSessionPaths.d.ts +40 -0
  131. package/build/tools/trace/resolveSessionPaths.test.d.ts +1 -0
  132. package/build/tools/trace/sessionState.d.ts +2 -0
  133. package/build/tools/trace/sessionState.test.d.ts +1 -0
  134. package/build/tools/trace/startTraceCollectionTool.d.ts +2 -0
  135. package/build/tools/trace/stopTraceCollectionTool.d.ts +2 -0
  136. package/build/tools/workspace/initScanWorkspaceTool.d.ts +4 -0
  137. package/build/tools/workspace/initializeWorkspaceTool.d.ts +2 -0
  138. package/build/tools/workspace/initializeWorkspaceTool.js +39 -22
  139. package/build/tools/workspace/initializeWorkspaceTool.test.d.ts +1 -0
  140. package/build/tools/workspace/initializeWorkspaceTool.test.js +134 -0
  141. package/build/tools/workspace/serviceUpsert.d.ts +12 -0
  142. package/build/tools/workspace/serviceUpsert.js +23 -0
  143. package/build/tools/workspace/serviceUpsert.test.d.ts +1 -0
  144. package/build/tools/workspace/serviceUpsert.test.js +50 -0
  145. package/build/types/OneClickCommands.d.ts +68 -0
  146. package/build/types/RepositoryAnalysis.d.ts +3004 -0
  147. package/build/types/TestAnalysis.d.ts +97 -0
  148. package/build/types/TestExecution.d.ts +63 -0
  149. package/build/types/TestExecution.js +11 -1
  150. package/build/types/TestRecommendation.d.ts +12 -0
  151. package/build/types/TestTypes.d.ts +234 -0
  152. package/build/types/TestbotReport.d.ts +68 -0
  153. package/build/types/TestbotReport.js +1 -0
  154. package/build/types/index.d.ts +4 -0
  155. package/build/types/index.js +3 -0
  156. package/build/utils/AnalysisStateManager.d.ts +242 -0
  157. package/build/utils/AnalysisStateManager.js +144 -3
  158. package/build/utils/AnalysisStateManager.test.d.ts +1 -0
  159. package/build/utils/AnalysisStateManager.test.js +98 -0
  160. package/build/utils/analyze-openapi.d.ts +1 -0
  161. package/build/utils/branchDiff.d.ts +18 -0
  162. package/build/utils/dartRouteExtractor.d.ts +45 -0
  163. package/build/utils/dartRouteExtractor.test.d.ts +1 -0
  164. package/build/utils/docker.d.ts +19 -0
  165. package/build/utils/docker.test.d.ts +1 -0
  166. package/build/utils/docker.test.js +10 -9
  167. package/build/utils/featureFlags.d.ts +37 -0
  168. package/build/utils/featureFlags.test.d.ts +1 -0
  169. package/build/utils/gitStaging.d.ts +24 -0
  170. package/build/utils/gitStaging.test.d.ts +1 -0
  171. package/build/utils/gitStaging.test.js +13 -7
  172. package/build/utils/httpDefaults.d.ts +10 -0
  173. package/build/utils/httpDefaults.test.d.ts +1 -0
  174. package/build/utils/initAgent.d.ts +36 -0
  175. package/build/utils/language-helper.d.ts +6 -0
  176. package/build/utils/logger.d.ts +11 -0
  177. package/build/utils/normalizeParams.d.ts +14 -0
  178. package/build/utils/normalizeSkyrampImports.d.ts +13 -0
  179. package/build/utils/pr-comment-parser.d.ts +31 -0
  180. package/build/utils/pr-comment-parser.test.d.ts +1 -0
  181. package/build/utils/pr-comment-parser.test.js +6 -5
  182. package/build/utils/projectMetadata.d.ts +15 -0
  183. package/build/utils/projectMetadata.test.d.ts +1 -0
  184. package/build/utils/proxy-terminal.d.ts +2 -0
  185. package/build/utils/repoScanner.d.ts +27 -0
  186. package/build/utils/repoScanner.test.d.ts +1 -0
  187. package/build/utils/routeParsers.d.ts +53 -0
  188. package/build/utils/routeParsers.js +31 -38
  189. package/build/utils/routeParsers.test.d.ts +1 -0
  190. package/build/utils/scenarioDrafting.d.ts +71 -0
  191. package/build/utils/scenarioDrafting.js +23 -1
  192. package/build/utils/scenarioDrafting.test.d.ts +1 -0
  193. package/build/utils/skyrampMdContent.d.ts +5 -0
  194. package/build/utils/sourceRouteExtractor.d.ts +48 -0
  195. package/build/utils/sourceRouteExtractor.test.d.ts +1 -0
  196. package/build/utils/telemetry.d.ts +32 -0
  197. package/build/utils/telemetry.test.d.ts +1 -0
  198. package/build/utils/trace-parser.d.ts +32 -0
  199. package/build/utils/trace-parser.test.d.ts +6 -0
  200. package/build/utils/uiPageEnumerator.d.ts +146 -0
  201. package/build/utils/uiPageEnumerator.js +58 -14
  202. package/build/utils/uiPageEnumerator.test.d.ts +1 -0
  203. package/build/utils/uiPageEnumerator.test.js +223 -43
  204. package/build/utils/utils.d.ts +45 -0
  205. package/build/utils/utils.js +10 -0
  206. package/build/utils/utils.test.d.ts +1 -0
  207. package/build/utils/versions.d.ts +3 -0
  208. package/build/utils/versions.js +1 -1
  209. package/build/utils/workspaceAuth.d.ts +127 -0
  210. package/build/utils/workspaceAuth.test.d.ts +1 -0
  211. package/build/utils/workspaceAuth.test.js +25 -25
  212. package/build/workspace/index.d.ts +1 -0
  213. package/build/workspace/workspace.d.ts +390 -0
  214. package/build/workspace/workspace.js +7 -7
  215. package/build/workspace/workspace.test.d.ts +1 -0
  216. package/build/workspace/workspace.test.js +6 -6
  217. package/node_modules/playwright/lib/mcp/skyramp/loadTraceTool.js +31 -13
  218. package/node_modules/playwright/lib/mcp/skyramp/traceRecordingBackend.js +88 -15
  219. package/node_modules/playwright/lib/mcp/test/skyRampExport.js +4 -3
  220. package/node_modules/playwright/node_modules/playwright-core/ThirdPartyNotices.txt +23 -126
  221. package/node_modules/playwright/node_modules/playwright-core/lib/generated/injectedScriptSource.js +1 -1
  222. package/node_modules/playwright/node_modules/playwright-core/lib/server/recorder/recorderApp.js +79 -19
  223. package/node_modules/playwright/node_modules/playwright-core/lib/server/recorder/skyramp/aliasRemapper.js +84 -0
  224. package/node_modules/playwright/node_modules/playwright-core/lib/server/recorder/skyramp/replayEngine.js +15 -4
  225. package/node_modules/playwright/node_modules/playwright-core/lib/vite/recorder/assets/{codeMirrorModule-CPZKmjFR.js → codeMirrorModule-D0BjbCb7.js} +1 -1
  226. package/node_modules/playwright/node_modules/playwright-core/lib/vite/recorder/assets/index-lvTRGFx-.js +193 -0
  227. package/node_modules/playwright/node_modules/playwright-core/lib/vite/recorder/index.html +1 -1
  228. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/assets/{codeMirrorModule-B0JOjboO.js → codeMirrorModule-Cqp3cwEJ.js} +1 -1
  229. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/assets/{defaultSettingsView-1anWeyDf.js → defaultSettingsView-CYf9adZh.js} +79 -79
  230. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/{index.Cc5029a3.js → index.B_7ywgmr.js} +1 -1
  231. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/index.html +2 -2
  232. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/uiMode.html +2 -2
  233. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/{uiMode.Wo5yvvVh.js → uiMode.u4_8VnCV.js} +1 -1
  234. package/node_modules/playwright/node_modules/playwright-core/package.json +1 -1
  235. package/node_modules/playwright/node_modules/playwright-core/src/generated/injectedScriptSource.ts +1 -1
  236. package/node_modules/playwright/node_modules/playwright-core/src/server/recorder/recorderApp.ts +129 -43
  237. package/node_modules/playwright/node_modules/playwright-core/src/server/recorder/skyramp/aliasRemapper.ts +119 -0
  238. package/node_modules/playwright/node_modules/playwright-core/src/server/recorder/skyramp/replayEngine.ts +33 -10
  239. package/node_modules/playwright/package.json +1 -1
  240. package/package.json +16 -4
  241. package/build/tools/test-management/uiAnalyzeChangesTool.js +0 -155
  242. package/build/tools/test-management/uiAnalyzeChangesTool.test.js +0 -147
  243. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/.bin/node-which +0 -52
  244. package/node_modules/playwright/node_modules/playwright-core/bundles/utils/node_modules/.bin/is-docker +0 -5
  245. package/node_modules/playwright/node_modules/playwright-core/bundles/utils/node_modules/.bin/mime +0 -46
  246. package/node_modules/playwright/node_modules/playwright-core/bundles/utils/node_modules/.bin/yaml +0 -11
  247. package/node_modules/playwright/node_modules/playwright-core/lib/vite/recorder/assets/index-CsgPR2wy.js +0 -193
  248. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/index.CP3Y8Zsb.js +0 -2
@@ -1,17 +1,20 @@
1
- // @ts-ignore
2
- import { registerSubmitReportTool, additionalRecommendationSchema } from "./submitReportTool.js";
1
+ // @ts-nocheck - Jest ESM type inference issues
2
+ import { jest } from "@jest/globals";
3
3
  import { TestType } from "../types/TestTypes.js";
4
4
  import { DriftAction } from "../types/TestAnalysis.js";
5
- import { AnalyticsService } from "../services/AnalyticsService.js";
5
+ import { TestExecutionStatus } from "../types/TestExecution.js";
6
6
  import * as fs from "fs/promises";
7
7
  import * as path from "path";
8
8
  import * as os from "os";
9
- jest.mock("../utils/logger.js", () => ({
10
- logger: { info: jest.fn(), error: jest.fn() },
9
+ jest.unstable_mockModule("../utils/logger.js", () => ({
10
+ logger: { info: jest.fn(), error: jest.fn(), warning: jest.fn(), debug: jest.fn() },
11
11
  }));
12
- jest.mock("../services/AnalyticsService.js", () => ({
12
+ jest.unstable_mockModule("../services/AnalyticsService.js", () => ({
13
13
  AnalyticsService: { pushMCPToolEvent: jest.fn().mockResolvedValue(undefined) },
14
14
  }));
15
+ // @ts-ignore
16
+ const { registerSubmitReportTool, additionalRecommendationSchema, newTestSchema, targetElementSchema, pageContextSchema } = await import("./submitReportTool.js");
17
+ const { AnalyticsService } = await import("../services/AnalyticsService.js");
15
18
  const pushMCPToolEventMock = AnalyticsService.pushMCPToolEvent;
16
19
  function captureToolHandler() {
17
20
  let handler;
@@ -23,14 +26,20 @@ function captureToolHandler() {
23
26
  registerSubmitReportTool(fakeServer);
24
27
  return handler;
25
28
  }
26
- 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) {
27
35
  return {
28
36
  summaryOutputFile: outputFile,
37
+ stateFile,
29
38
  businessCaseAnalysis: "This PR adds product search. Tests needed for filtering.",
30
39
  newTestsCreated: [
31
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" },
32
41
  ],
33
- 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: "" }],
34
43
  testResults: [
35
44
  { testType: TestType.SMOKE, endpoint: "GET /api/v1/products", status: "Pass", details: "2.1s, products_smoke_test.py" },
36
45
  { testType: TestType.SMOKE, endpoint: "GET /api/v1/products/search", status: "Fail", details: "3.4s, search_smoke_test.py" },
@@ -65,7 +74,8 @@ describe("registerSubmitReportTool", () => {
65
74
  const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
66
75
  tmpDirs.push(tmpDir);
67
76
  const outputFile = path.join(tmpDir, "report.json");
68
- const result = await handler(sampleReportParams(outputFile));
77
+ const stateFile = await writeStateFile(tmpDir, []);
78
+ const result = await handler(sampleReportParams(outputFile, stateFile));
69
79
  expect(result.isError).toBeUndefined();
70
80
  expect(result.content[0].text).toContain("Report submitted successfully");
71
81
  const written = JSON.parse(await fs.readFile(outputFile, "utf-8"));
@@ -75,11 +85,44 @@ describe("registerSubmitReportTool", () => {
75
85
  expect(written.issuesFound).toHaveLength(1);
76
86
  expect(written.testMaintenance).toHaveLength(1);
77
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
+ });
78
120
  it("should create parent directories if they don't exist", async () => {
79
121
  const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
80
122
  tmpDirs.push(tmpDir);
81
123
  const outputFile = path.join(tmpDir, "nested", "dirs", "report.json");
82
- const result = await handler(sampleReportParams(outputFile));
124
+ const stateFile = await writeStateFile(tmpDir, []);
125
+ const result = await handler(sampleReportParams(outputFile, stateFile));
83
126
  expect(result.isError).toBeUndefined();
84
127
  const written = JSON.parse(await fs.readFile(outputFile, "utf-8"));
85
128
  expect(written.businessCaseAnalysis).toBeDefined();
@@ -88,11 +131,12 @@ describe("registerSubmitReportTool", () => {
88
131
  const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
89
132
  tmpDirs.push(tmpDir);
90
133
  const outputFile = path.join(tmpDir, "report.json");
134
+ const stateFile = await writeStateFile(tmpDir, []);
91
135
  const result = await handler({
92
136
  summaryOutputFile: outputFile,
137
+ stateFile,
93
138
  businessCaseAnalysis: "Simple config change.",
94
139
  newTestsCreated: [],
95
- testMaintenance: [],
96
140
  testResults: [
97
141
  { testType: TestType.SMOKE, endpoint: "GET /api/v1/products", status: "Pass", details: "1.0s, test.py" },
98
142
  ],
@@ -101,12 +145,15 @@ describe("registerSubmitReportTool", () => {
101
145
  expect(result.isError).toBeUndefined();
102
146
  const written = JSON.parse(await fs.readFile(outputFile, "utf-8"));
103
147
  expect(written.newTestsCreated).toEqual([]);
104
- expect(written.testMaintenance).toEqual([]);
148
+ expect(written.testMaintenance).toBeUndefined();
105
149
  expect(written.issuesFound).toEqual([]);
106
150
  expect(written.testResults).toHaveLength(1);
107
151
  });
108
152
  it("should return error for invalid file path", async () => {
109
- const result = await handler(sampleReportParams("/nonexistent/readonly/path/report.json"));
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));
110
157
  expect(result.isError).toBe(true);
111
158
  expect(result.content[0].text).toContain("Failed to write report");
112
159
  });
@@ -114,8 +161,9 @@ describe("registerSubmitReportTool", () => {
114
161
  const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
115
162
  tmpDirs.push(tmpDir);
116
163
  const outputFile = path.join(tmpDir, "report.json");
164
+ const stateFile = await writeStateFile(tmpDir, []);
117
165
  const result = await handler({
118
- ...sampleReportParams(outputFile),
166
+ ...sampleReportParams(outputFile, stateFile),
119
167
  commitMessage: "add contract tests for /products endpoint",
120
168
  });
121
169
  expect(result.isError).toBeUndefined();
@@ -126,7 +174,8 @@ describe("registerSubmitReportTool", () => {
126
174
  const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
127
175
  tmpDirs.push(tmpDir);
128
176
  const outputFile = path.join(tmpDir, "report.json");
129
- const result = await handler(sampleReportParams(outputFile));
177
+ const stateFile = await writeStateFile(tmpDir, []);
178
+ const result = await handler(sampleReportParams(outputFile, stateFile));
130
179
  expect(result.isError).toBeUndefined();
131
180
  const written = JSON.parse(await fs.readFile(outputFile, "utf-8"));
132
181
  expect(written.commitMessage).toBe("Added recommendations by Skyramp Testbot.");
@@ -135,8 +184,9 @@ describe("registerSubmitReportTool", () => {
135
184
  const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
136
185
  tmpDirs.push(tmpDir);
137
186
  const outputFile = path.join(tmpDir, "report.json");
187
+ const stateFile = await writeStateFile(tmpDir, []);
138
188
  const result = await handler({
139
- ...sampleReportParams(outputFile),
189
+ ...sampleReportParams(outputFile, stateFile),
140
190
  commitMessage: " line one\nline two\r\nline three ",
141
191
  });
142
192
  expect(result.isError).toBeUndefined();
@@ -148,9 +198,10 @@ describe("registerSubmitReportTool", () => {
148
198
  const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
149
199
  tmpDirs.push(tmpDir);
150
200
  const outputFile = path.join(tmpDir, "report.json");
201
+ const stateFile = await writeStateFile(tmpDir, []);
151
202
  const longMessage = "add contract and integration tests for the new PATCH /orders/{order_id} endpoint including discount recalculation and line-item edits";
152
203
  const result = await handler({
153
- ...sampleReportParams(outputFile),
204
+ ...sampleReportParams(outputFile, stateFile),
154
205
  commitMessage: longMessage,
155
206
  });
156
207
  expect(result.isError).toBeUndefined();
@@ -158,24 +209,13 @@ describe("registerSubmitReportTool", () => {
158
209
  expect(written.commitMessage).toBe(longMessage);
159
210
  expect(written.commitMessage.length).toBeGreaterThan(72);
160
211
  });
161
- it("should use default commitMessage when provided as empty string", async () => {
162
- const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
163
- tmpDirs.push(tmpDir);
164
- const outputFile = path.join(tmpDir, "report.json");
165
- const result = await handler({
166
- ...sampleReportParams(outputFile),
167
- commitMessage: "",
168
- });
169
- expect(result.isError).toBeUndefined();
170
- const written = JSON.parse(await fs.readFile(outputFile, "utf-8"));
171
- expect(written.commitMessage).toBe("Added recommendations by Skyramp Testbot.");
172
- });
173
212
  it("should use default commitMessage when whitespace-only", async () => {
174
213
  const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
175
214
  tmpDirs.push(tmpDir);
176
215
  const outputFile = path.join(tmpDir, "report.json");
216
+ const stateFile = await writeStateFile(tmpDir, []);
177
217
  const result = await handler({
178
- ...sampleReportParams(outputFile),
218
+ ...sampleReportParams(outputFile, stateFile),
179
219
  commitMessage: " \n\r\n ",
180
220
  });
181
221
  expect(result.isError).toBeUndefined();
@@ -186,8 +226,9 @@ describe("registerSubmitReportTool", () => {
186
226
  const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
187
227
  tmpDirs.push(tmpDir);
188
228
  const outputFile = path.join(tmpDir, "report.json");
229
+ const stateFile = await writeStateFile(tmpDir, []);
189
230
  const result = await handler({
190
- ...sampleReportParams(outputFile),
231
+ ...sampleReportParams(outputFile, stateFile),
191
232
  nextSteps: ["Check your targetSetupCommand — endpoints returned 404"],
192
233
  });
193
234
  expect(result.isError).toBeUndefined();
@@ -200,7 +241,8 @@ describe("registerSubmitReportTool", () => {
200
241
  const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
201
242
  tmpDirs.push(tmpDir);
202
243
  const outputFile = path.join(tmpDir, "report.json");
203
- const result = await handler(sampleReportParams(outputFile));
244
+ const stateFile = await writeStateFile(tmpDir, []);
245
+ const result = await handler(sampleReportParams(outputFile, stateFile));
204
246
  expect(result.isError).toBeUndefined();
205
247
  const written = JSON.parse(await fs.readFile(outputFile, "utf-8"));
206
248
  expect(written.nextSteps).toEqual([]);
@@ -209,7 +251,8 @@ describe("registerSubmitReportTool", () => {
209
251
  const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
210
252
  tmpDirs.push(tmpDir);
211
253
  const outputFile = path.join(tmpDir, "report.json");
212
- await handler(sampleReportParams(outputFile));
254
+ const stateFile = await writeStateFile(tmpDir, []);
255
+ await handler(sampleReportParams(outputFile, stateFile));
213
256
  const raw = await fs.readFile(outputFile, "utf-8");
214
257
  // Pretty-printed JSON should have indentation
215
258
  expect(raw).toContain(" ");
@@ -220,8 +263,9 @@ describe("registerSubmitReportTool", () => {
220
263
  const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
221
264
  tmpDirs.push(tmpDir);
222
265
  const outputFile = path.join(tmpDir, "report.json");
266
+ const stateFile = await writeStateFile(tmpDir, []);
223
267
  const params = {
224
- ...sampleReportParams(outputFile),
268
+ ...sampleReportParams(outputFile, stateFile),
225
269
  additionalRecommendations: [
226
270
  {
227
271
  testId: "integration-orders-patch-status",
@@ -293,6 +337,214 @@ describe("registerSubmitReportTool", () => {
293
337
  expect(result.data.primaryEndpoint).toBeUndefined();
294
338
  }
295
339
  });
340
+ // Structured grounding fields for UI recommendations.
341
+ // ----------------------------------------------------------------------
342
+ // Sub-schemas — targetElement and pageContext — populated by the agent
343
+ // from a captured browser_blueprint element. UI-only via superRefine on
344
+ // the parent newTestSchema / additionalRecommendationSchema.
345
+ describe("targetElementSchema", () => {
346
+ const validTarget = {
347
+ role: "button",
348
+ accessibleName: "Save changes",
349
+ testId: "save-changes-btn",
350
+ stableId: null,
351
+ contextText: null,
352
+ mutability: "mutable",
353
+ widgetType: "native",
354
+ };
355
+ it("accepts a fully-populated targetElement", () => {
356
+ expect(targetElementSchema.safeParse(validTarget).success).toBe(true);
357
+ });
358
+ it("accepts a minimal targetElement (only required fields)", () => {
359
+ const result = targetElementSchema.safeParse({
360
+ role: "heading",
361
+ accessibleName: "Notifications",
362
+ testId: null,
363
+ stableId: null,
364
+ contextText: null,
365
+ });
366
+ expect(result.success).toBe(true);
367
+ });
368
+ it("accepts contextText as a string array (repeating-row case)", () => {
369
+ const result = targetElementSchema.safeParse({
370
+ ...validTarget,
371
+ contextText: ["customer@example.com", "$129.99", "Pending"],
372
+ });
373
+ expect(result.success).toBe(true);
374
+ });
375
+ it("rejects targetElement missing accessibleName", () => {
376
+ const { accessibleName, ...withoutName } = validTarget;
377
+ const result = targetElementSchema.safeParse(withoutName);
378
+ expect(result.success).toBe(false);
379
+ });
380
+ it("rejects targetElement missing role", () => {
381
+ const { role, ...withoutRole } = validTarget;
382
+ const result = targetElementSchema.safeParse(withoutRole);
383
+ expect(result.success).toBe(false);
384
+ });
385
+ it("rejects an invalid mutability enum value", () => {
386
+ const result = targetElementSchema.safeParse({ ...validTarget, mutability: "sometimes" });
387
+ expect(result.success).toBe(false);
388
+ });
389
+ it("rejects an invalid widgetType enum value", () => {
390
+ const result = targetElementSchema.safeParse({ ...validTarget, widgetType: "fancy" });
391
+ expect(result.success).toBe(false);
392
+ });
393
+ });
394
+ describe("pageContextSchema (UI recommendation grounding)", () => {
395
+ it("accepts pageContext with both url and pageHash", () => {
396
+ const result = pageContextSchema.safeParse({
397
+ url: "http://localhost:5173/orders/1",
398
+ pageHash: "10:434266447",
399
+ });
400
+ expect(result.success).toBe(true);
401
+ });
402
+ it("accepts pageContext without pageHash (optional)", () => {
403
+ const result = pageContextSchema.safeParse({ url: "http://localhost:5173/orders/1" });
404
+ expect(result.success).toBe(true);
405
+ });
406
+ it("rejects pageContext missing url", () => {
407
+ const result = pageContextSchema.safeParse({ pageHash: "10:abc" });
408
+ expect(result.success).toBe(false);
409
+ });
410
+ });
411
+ describe("newTestSchema with grounding fields (UI recommendation grounding)", () => {
412
+ const validTarget = {
413
+ role: "button",
414
+ accessibleName: "Save changes",
415
+ testId: "save-changes-btn",
416
+ stableId: null,
417
+ contextText: null,
418
+ };
419
+ const validPage = {
420
+ url: "http://localhost:5173/orders/1",
421
+ pageHash: "10:434266447",
422
+ };
423
+ const baseUITest = {
424
+ testId: "ui-edit-order-discount-validation",
425
+ testType: TestType.UI,
426
+ category: "business_rule",
427
+ endpoint: "PATCH /api/v1/orders/{id}",
428
+ fileName: "edit_order_discount.spec.ts",
429
+ description: "Navigates to the Edit Order form, enters a discount > 100, and asserts that the validation error appears and the Save button is disabled.",
430
+ reasoning: "Verifies the **Save changes** button (testid `save-changes-btn`) on the Edit Order form shows the validation error when discount > 100.",
431
+ };
432
+ it("accepts a UI test with single-element targetElements + pageContext", () => {
433
+ const result = newTestSchema.safeParse({
434
+ ...baseUITest,
435
+ targetElements: [validTarget],
436
+ pageContext: validPage,
437
+ });
438
+ expect(result.success).toBe(true);
439
+ });
440
+ it("accepts a UI test with multi-element targetElements (render-state test)", () => {
441
+ const result = newTestSchema.safeParse({
442
+ ...baseUITest,
443
+ targetElements: [
444
+ {
445
+ role: "heading",
446
+ accessibleName: "Edit Order",
447
+ testId: "edit-order-heading",
448
+ stableId: null,
449
+ contextText: null,
450
+ },
451
+ {
452
+ role: "spinbutton",
453
+ accessibleName: "Discount (%)",
454
+ testId: "edit-order-input-discount",
455
+ stableId: null,
456
+ contextText: null,
457
+ },
458
+ ],
459
+ pageContext: validPage,
460
+ });
461
+ expect(result.success).toBe(true);
462
+ });
463
+ it("rejects a UI test without targetElements (required for UI grounding)", () => {
464
+ const result = newTestSchema.safeParse(baseUITest);
465
+ expect(result.success).toBe(false);
466
+ expect(JSON.stringify(result.error?.issues)).toContain("targetElements is required for testType: 'ui'");
467
+ });
468
+ it("accepts a UI test with targetElements: null (fallback case)", () => {
469
+ const result = newTestSchema.safeParse({
470
+ ...baseUITest,
471
+ targetElements: null,
472
+ description: "[no-blueprint-data] Navigates to Edit Order form and checks discount validation error.",
473
+ reasoning: "[no-blueprint-data] Verifies the discount validation error appears on the Edit Order form when discount > 100.",
474
+ });
475
+ expect(result.success).toBe(true);
476
+ });
477
+ it("rejects a contract test that includes targetElements (UI-only)", () => {
478
+ const result = newTestSchema.safeParse({
479
+ testId: "contract-orders-patch",
480
+ testType: TestType.CONTRACT,
481
+ category: "business_rule",
482
+ endpoint: "PATCH /api/v1/orders/{id}",
483
+ fileName: "orders_patch_contract.py",
484
+ reasoning: "Validates request shape and 200/422 status codes",
485
+ targetElements: [validTarget],
486
+ });
487
+ expect(result.success).toBe(false);
488
+ if (!result.success) {
489
+ expect(result.error.issues.some((i) => i.path.includes("targetElements"))).toBe(true);
490
+ }
491
+ });
492
+ it("rejects an integration test that includes pageContext (UI-only)", () => {
493
+ const result = newTestSchema.safeParse({
494
+ testId: "integration-orders",
495
+ testType: TestType.INTEGRATION,
496
+ category: "business_rule",
497
+ endpoint: "POST /api/v1/orders",
498
+ fileName: "orders_integration.py",
499
+ reasoning: "Multi-step order lifecycle",
500
+ pageContext: validPage,
501
+ });
502
+ expect(result.success).toBe(false);
503
+ if (!result.success) {
504
+ expect(result.error.issues.some((i) => i.path.includes("pageContext"))).toBe(true);
505
+ }
506
+ });
507
+ });
508
+ describe("additionalRecommendationSchema with grounding fields (UI recommendation grounding)", () => {
509
+ const validTarget = {
510
+ role: "button",
511
+ accessibleName: "Save changes",
512
+ testId: "save-changes-btn",
513
+ stableId: null,
514
+ contextText: null,
515
+ };
516
+ const validPage = { url: "http://localhost:5173/orders/1" };
517
+ it("accepts a UI rec with targetElements + pageContext", () => {
518
+ const result = additionalRecommendationSchema.safeParse({
519
+ testId: "ui-edit-order-discount-validation",
520
+ testType: TestType.UI,
521
+ category: "business_rule",
522
+ steps: [{ description: "Open Edit Order, type 150 in discount, click Save" }],
523
+ description: "Verify discount > 100 shows validation error",
524
+ priority: "medium",
525
+ reasoning: "Verify that the **Save changes** button (testid `save-changes-btn`) on the Edit Order form shows the validation error when discount > 100.",
526
+ targetElements: [validTarget],
527
+ pageContext: validPage,
528
+ });
529
+ expect(result.success).toBe(true);
530
+ });
531
+ it("rejects a non-UI rec that includes targetElements", () => {
532
+ const result = additionalRecommendationSchema.safeParse({
533
+ testId: "integration-orders-patch",
534
+ testType: TestType.INTEGRATION,
535
+ category: "business_rule",
536
+ steps: [{ method: "PATCH", path: "/api/v1/orders/1", description: "Patch order" }],
537
+ description: "Verifies PATCH workflow",
538
+ priority: "high",
539
+ reasoning: "Catches mutation bugs",
540
+ targetElements: [validTarget],
541
+ });
542
+ expect(result.success).toBe(false);
543
+ if (!result.success) {
544
+ expect(result.error.issues.some((i) => i.path.includes("targetElements"))).toBe(true);
545
+ }
546
+ });
547
+ });
296
548
  describe("analytics metrics", () => {
297
549
  beforeEach(() => {
298
550
  pushMCPToolEventMock.mockClear();
@@ -306,14 +558,15 @@ describe("registerSubmitReportTool", () => {
306
558
  const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
307
559
  tmpDirs.push(tmpDir);
308
560
  const outputFile = path.join(tmpDir, "report.json");
309
- // sampleReportParams: 1 new test, 1 maintenance (Fail→Pass), 2 results
561
+ // sampleReportParams: 1 new test, 1 maintenance (no matching stateFile entry), 2 results
310
562
  // (1 Pass + 1 Fail), 1 issue (no severity).
311
- await handler(sampleReportParams(outputFile));
563
+ const stateFile = await writeStateFile(tmpDir, []);
564
+ await handler(sampleReportParams(outputFile, stateFile));
312
565
  const params = lastAnalyticsParams();
313
566
  expect(params.testsGenerated).toBe("1");
314
567
  expect(params.testsMaintained).toBe("1");
315
568
  expect(params.recommendationsCount).toBe("0");
316
- expect(params.maintenanceRecovered).toBe("1");
569
+ expect(params.maintenanceRecovered).toBe("0"); // no matching stateFile entry → no execution data
317
570
  expect(params.testsPassed).toBe("1");
318
571
  expect(params.testsFailed).toBe("1");
319
572
  expect(params.testsSkipped).toBe("0");
@@ -323,8 +576,9 @@ describe("registerSubmitReportTool", () => {
323
576
  const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
324
577
  tmpDirs.push(tmpDir);
325
578
  const outputFile = path.join(tmpDir, "report.json");
579
+ const stateFile = await writeStateFile(tmpDir, []);
326
580
  await handler({
327
- ...sampleReportParams(outputFile),
581
+ ...sampleReportParams(outputFile, stateFile),
328
582
  issuesFound: [
329
583
  { description: "Data corruption on checkout", severity: "critical" },
330
584
  { description: "Wrong total returned", severity: "high" },
@@ -344,8 +598,10 @@ describe("registerSubmitReportTool", () => {
344
598
  const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
345
599
  tmpDirs.push(tmpDir);
346
600
  const outputFile = path.join(tmpDir, "report.json");
601
+ const stateFile = await writeStateFile(tmpDir, []);
347
602
  await handler({
348
603
  summaryOutputFile: outputFile,
604
+ stateFile,
349
605
  businessCaseAnalysis: "Config-only change.",
350
606
  newTestsCreated: [],
351
607
  testMaintenance: [],
@@ -363,36 +619,236 @@ describe("registerSubmitReportTool", () => {
363
619
  const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
364
620
  tmpDirs.push(tmpDir);
365
621
  const outputFile = path.join(tmpDir, "report.json");
622
+ const stateFile = await writeStateFile(tmpDir, []);
366
623
  await handler({
367
- ...sampleReportParams(outputFile),
624
+ ...sampleReportParams(outputFile, stateFile),
368
625
  testMaintenance: [
369
626
  // Real change
370
- { 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)" },
371
- { 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)" },
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: "" },
372
629
  // No-ops — assessed but not modified
373
- { 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" },
374
- { 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" },
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: "" },
375
632
  ],
376
633
  });
377
634
  const params = lastAnalyticsParams();
378
635
  // 2 change actions (UPDATE + REGENERATE), not 4
379
636
  expect(params.testsMaintained).toBe("2");
380
- // Both changes went Fail→Pass
381
- expect(params.maintenanceRecovered).toBe("2");
637
+ // No matching stateFile entries — execution data unavailable
638
+ expect(params.maintenanceRecovered).toBe("0");
382
639
  });
383
- it("counts UPDATE/REGENERATE/DELETE as maintenance changes, not VERIFY/IGNORE", async () => {
384
- const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
385
- tmpDirs.push(tmpDir);
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) {
386
657
  const outputFile = path.join(tmpDir, "report.json");
387
658
  await handler({
388
- ...sampleReportParams(outputFile),
389
- testMaintenance: [
390
- { 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" },
391
- { 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" },
392
- ],
659
+ summaryOutputFile: outputFile,
660
+ businessCaseAnalysis: "Test",
661
+ newTestsCreated: [],
662
+ testMaintenance: maintenance,
663
+ testResults: [],
664
+ issuesFound: [],
665
+ ...extra,
393
666
  });
394
- const params = lastAnalyticsParams();
395
- expect(params.testsMaintained).toBe("1");
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("");
396
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");
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();
397
853
  });
398
854
  });