@skyramp/mcp 0.2.6 → 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 (233) 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 +7 -16
  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 +6 -99
  57. package/build/prompts/testbot/testbot-prompts.d.ts +11 -0
  58. package/build/prompts/testbot/testbot-prompts.js +213 -42
  59. package/build/prompts/testbot/testbot-prompts.test.d.ts +1 -0
  60. package/build/prompts/testbot/testbot-prompts.test.js +15 -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 +1 -1
  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 +17 -4
  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 +384 -0
  115. package/build/tools/submitReportTool.js +225 -37
  116. package/build/tools/submitReportTool.test.d.ts +1 -0
  117. package/build/tools/submitReportTool.test.js +479 -55
  118. package/build/tools/test-management/actionsTool.d.ts +10 -0
  119. package/build/tools/test-management/actionsTool.js +10 -0
  120. package/build/tools/test-management/analyzeChangesTool.d.ts +26 -0
  121. package/build/tools/test-management/analyzeChangesTool.js +128 -48
  122. package/build/tools/test-management/analyzeChangesTool.test.d.ts +1 -0
  123. package/build/tools/test-management/analyzeChangesTool.test.js +80 -73
  124. package/build/tools/test-management/analyzeTestHealthTool.d.ts +2 -0
  125. package/build/tools/test-management/analyzeTestHealthTool.js +6 -3
  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.test.d.ts +1 -0
  139. package/build/tools/workspace/initializeWorkspaceTool.test.js +134 -0
  140. package/build/types/OneClickCommands.d.ts +68 -0
  141. package/build/types/RepositoryAnalysis.d.ts +3004 -0
  142. package/build/types/TestAnalysis.d.ts +97 -0
  143. package/build/types/TestExecution.d.ts +63 -0
  144. package/build/types/TestExecution.js +11 -1
  145. package/build/types/TestRecommendation.d.ts +12 -0
  146. package/build/types/TestTypes.d.ts +234 -0
  147. package/build/types/TestbotReport.d.ts +68 -0
  148. package/build/types/TestbotReport.js +1 -0
  149. package/build/types/index.d.ts +4 -0
  150. package/build/types/index.js +3 -0
  151. package/build/utils/AnalysisStateManager.d.ts +182 -0
  152. package/build/utils/AnalysisStateManager.js +10 -1
  153. package/build/utils/AnalysisStateManager.test.d.ts +1 -0
  154. package/build/utils/analyze-openapi.d.ts +1 -0
  155. package/build/utils/branchDiff.d.ts +18 -0
  156. package/build/utils/dartRouteExtractor.d.ts +45 -0
  157. package/build/utils/dartRouteExtractor.test.d.ts +1 -0
  158. package/build/utils/docker.d.ts +19 -0
  159. package/build/utils/docker.test.d.ts +1 -0
  160. package/build/utils/docker.test.js +10 -9
  161. package/build/utils/featureFlags.d.ts +37 -0
  162. package/build/utils/featureFlags.test.d.ts +1 -0
  163. package/build/utils/gitStaging.d.ts +24 -0
  164. package/build/utils/gitStaging.test.d.ts +1 -0
  165. package/build/utils/gitStaging.test.js +13 -7
  166. package/build/utils/httpDefaults.d.ts +10 -0
  167. package/build/utils/httpDefaults.test.d.ts +1 -0
  168. package/build/utils/initAgent.d.ts +36 -0
  169. package/build/utils/language-helper.d.ts +6 -0
  170. package/build/utils/logger.d.ts +11 -0
  171. package/build/utils/normalizeParams.d.ts +14 -0
  172. package/build/utils/normalizeSkyrampImports.d.ts +13 -0
  173. package/build/utils/pr-comment-parser.d.ts +31 -0
  174. package/build/utils/pr-comment-parser.test.d.ts +1 -0
  175. package/build/utils/pr-comment-parser.test.js +6 -5
  176. package/build/utils/projectMetadata.d.ts +15 -0
  177. package/build/utils/projectMetadata.test.d.ts +1 -0
  178. package/build/utils/proxy-terminal.d.ts +2 -0
  179. package/build/utils/repoScanner.d.ts +27 -0
  180. package/build/utils/repoScanner.test.d.ts +1 -0
  181. package/build/utils/routeParsers.d.ts +53 -0
  182. package/build/utils/routeParsers.js +31 -38
  183. package/build/utils/routeParsers.test.d.ts +1 -0
  184. package/build/utils/scenarioDrafting.d.ts +71 -0
  185. package/build/utils/scenarioDrafting.js +23 -1
  186. package/build/utils/scenarioDrafting.test.d.ts +1 -0
  187. package/build/utils/skyrampMdContent.d.ts +5 -0
  188. package/build/utils/sourceRouteExtractor.d.ts +48 -0
  189. package/build/utils/sourceRouteExtractor.test.d.ts +1 -0
  190. package/build/utils/telemetry.d.ts +32 -0
  191. package/build/utils/telemetry.test.d.ts +1 -0
  192. package/build/utils/trace-parser.d.ts +32 -0
  193. package/build/utils/trace-parser.test.d.ts +6 -0
  194. package/build/utils/uiPageEnumerator.d.ts +146 -0
  195. package/build/utils/uiPageEnumerator.js +58 -14
  196. package/build/utils/uiPageEnumerator.test.d.ts +1 -0
  197. package/build/utils/uiPageEnumerator.test.js +223 -43
  198. package/build/utils/utils.d.ts +45 -0
  199. package/build/utils/utils.js +10 -0
  200. package/build/utils/utils.test.d.ts +1 -0
  201. package/build/utils/versions.d.ts +3 -0
  202. package/build/utils/versions.js +1 -1
  203. package/build/utils/workspaceAuth.d.ts +127 -0
  204. package/build/utils/workspaceAuth.test.d.ts +1 -0
  205. package/build/utils/workspaceAuth.test.js +25 -25
  206. package/build/workspace/index.d.ts +1 -0
  207. package/build/workspace/workspace.d.ts +390 -0
  208. package/build/workspace/workspace.test.d.ts +1 -0
  209. package/node_modules/playwright/LICENSE +202 -0
  210. package/node_modules/playwright/NOTICE +5 -0
  211. package/node_modules/playwright/README.md +168 -0
  212. package/node_modules/playwright/lib/mcp/skyramp/traceRecordingBackend.js +13 -9
  213. package/node_modules/playwright/lib/mcp/test/skyRampExport.js +4 -3
  214. package/node_modules/playwright/node_modules/playwright-core/LICENSE +202 -0
  215. package/node_modules/playwright/node_modules/playwright-core/NOTICE +5 -0
  216. package/node_modules/playwright/node_modules/playwright-core/ThirdPartyNotices.txt +23 -126
  217. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/assets/{codeMirrorModule-B0JOjboO.js → codeMirrorModule-B5kqh2EV.js} +1 -1
  218. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/assets/{defaultSettingsView-1anWeyDf.js → defaultSettingsView-CZ9npQ3N.js} +79 -79
  219. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/{index.Cc5029a3.js → index.BCnxj-_b.js} +1 -1
  220. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/index.html +2 -2
  221. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/{uiMode.Wo5yvvVh.js → uiMode.1Ym0Ivn8.js} +1 -1
  222. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/uiMode.html +2 -2
  223. package/node_modules/playwright/node_modules/playwright-core/package.json +1 -1
  224. package/node_modules/playwright/package.json +1 -1
  225. package/package.json +16 -4
  226. package/build/tools/test-management/uiAnalyzeChangesTool.js +0 -155
  227. package/build/tools/test-management/uiAnalyzeChangesTool.test.js +0 -147
  228. package/node_modules/playwright/node_modules/playwright-core/.DS_Store +0 -0
  229. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/.bin/node-which +0 -52
  230. package/node_modules/playwright/node_modules/playwright-core/bundles/utils/node_modules/.bin/is-docker +0 -5
  231. package/node_modules/playwright/node_modules/playwright-core/bundles/utils/node_modules/.bin/mime +0 -46
  232. package/node_modules/playwright/node_modules/playwright-core/bundles/utils/node_modules/.bin/yaml +0 -11
  233. 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"));
@@ -79,7 +89,8 @@ describe("registerSubmitReportTool", () => {
79
89
  const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
80
90
  tmpDirs.push(tmpDir);
81
91
  const outputFile = path.join(tmpDir, "nested", "dirs", "report.json");
82
- const result = await handler(sampleReportParams(outputFile));
92
+ const stateFile = await writeStateFile(tmpDir, []);
93
+ const result = await handler(sampleReportParams(outputFile, stateFile));
83
94
  expect(result.isError).toBeUndefined();
84
95
  const written = JSON.parse(await fs.readFile(outputFile, "utf-8"));
85
96
  expect(written.businessCaseAnalysis).toBeDefined();
@@ -88,11 +99,12 @@ describe("registerSubmitReportTool", () => {
88
99
  const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
89
100
  tmpDirs.push(tmpDir);
90
101
  const outputFile = path.join(tmpDir, "report.json");
102
+ const stateFile = await writeStateFile(tmpDir, []);
91
103
  const result = await handler({
92
104
  summaryOutputFile: outputFile,
105
+ stateFile,
93
106
  businessCaseAnalysis: "Simple config change.",
94
107
  newTestsCreated: [],
95
- testMaintenance: [],
96
108
  testResults: [
97
109
  { testType: TestType.SMOKE, endpoint: "GET /api/v1/products", status: "Pass", details: "1.0s, test.py" },
98
110
  ],
@@ -101,12 +113,15 @@ describe("registerSubmitReportTool", () => {
101
113
  expect(result.isError).toBeUndefined();
102
114
  const written = JSON.parse(await fs.readFile(outputFile, "utf-8"));
103
115
  expect(written.newTestsCreated).toEqual([]);
104
- expect(written.testMaintenance).toEqual([]);
116
+ expect(written.testMaintenance).toBeUndefined();
105
117
  expect(written.issuesFound).toEqual([]);
106
118
  expect(written.testResults).toHaveLength(1);
107
119
  });
108
120
  it("should return error for invalid file path", async () => {
109
- 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));
110
125
  expect(result.isError).toBe(true);
111
126
  expect(result.content[0].text).toContain("Failed to write report");
112
127
  });
@@ -114,8 +129,9 @@ describe("registerSubmitReportTool", () => {
114
129
  const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
115
130
  tmpDirs.push(tmpDir);
116
131
  const outputFile = path.join(tmpDir, "report.json");
132
+ const stateFile = await writeStateFile(tmpDir, []);
117
133
  const result = await handler({
118
- ...sampleReportParams(outputFile),
134
+ ...sampleReportParams(outputFile, stateFile),
119
135
  commitMessage: "add contract tests for /products endpoint",
120
136
  });
121
137
  expect(result.isError).toBeUndefined();
@@ -126,7 +142,8 @@ describe("registerSubmitReportTool", () => {
126
142
  const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
127
143
  tmpDirs.push(tmpDir);
128
144
  const outputFile = path.join(tmpDir, "report.json");
129
- const result = await handler(sampleReportParams(outputFile));
145
+ const stateFile = await writeStateFile(tmpDir, []);
146
+ const result = await handler(sampleReportParams(outputFile, stateFile));
130
147
  expect(result.isError).toBeUndefined();
131
148
  const written = JSON.parse(await fs.readFile(outputFile, "utf-8"));
132
149
  expect(written.commitMessage).toBe("Added recommendations by Skyramp Testbot.");
@@ -135,8 +152,9 @@ describe("registerSubmitReportTool", () => {
135
152
  const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
136
153
  tmpDirs.push(tmpDir);
137
154
  const outputFile = path.join(tmpDir, "report.json");
155
+ const stateFile = await writeStateFile(tmpDir, []);
138
156
  const result = await handler({
139
- ...sampleReportParams(outputFile),
157
+ ...sampleReportParams(outputFile, stateFile),
140
158
  commitMessage: " line one\nline two\r\nline three ",
141
159
  });
142
160
  expect(result.isError).toBeUndefined();
@@ -148,9 +166,10 @@ describe("registerSubmitReportTool", () => {
148
166
  const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
149
167
  tmpDirs.push(tmpDir);
150
168
  const outputFile = path.join(tmpDir, "report.json");
169
+ const stateFile = await writeStateFile(tmpDir, []);
151
170
  const longMessage = "add contract and integration tests for the new PATCH /orders/{order_id} endpoint including discount recalculation and line-item edits";
152
171
  const result = await handler({
153
- ...sampleReportParams(outputFile),
172
+ ...sampleReportParams(outputFile, stateFile),
154
173
  commitMessage: longMessage,
155
174
  });
156
175
  expect(result.isError).toBeUndefined();
@@ -158,24 +177,13 @@ describe("registerSubmitReportTool", () => {
158
177
  expect(written.commitMessage).toBe(longMessage);
159
178
  expect(written.commitMessage.length).toBeGreaterThan(72);
160
179
  });
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
180
  it("should use default commitMessage when whitespace-only", async () => {
174
181
  const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
175
182
  tmpDirs.push(tmpDir);
176
183
  const outputFile = path.join(tmpDir, "report.json");
184
+ const stateFile = await writeStateFile(tmpDir, []);
177
185
  const result = await handler({
178
- ...sampleReportParams(outputFile),
186
+ ...sampleReportParams(outputFile, stateFile),
179
187
  commitMessage: " \n\r\n ",
180
188
  });
181
189
  expect(result.isError).toBeUndefined();
@@ -186,8 +194,9 @@ describe("registerSubmitReportTool", () => {
186
194
  const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
187
195
  tmpDirs.push(tmpDir);
188
196
  const outputFile = path.join(tmpDir, "report.json");
197
+ const stateFile = await writeStateFile(tmpDir, []);
189
198
  const result = await handler({
190
- ...sampleReportParams(outputFile),
199
+ ...sampleReportParams(outputFile, stateFile),
191
200
  nextSteps: ["Check your targetSetupCommand — endpoints returned 404"],
192
201
  });
193
202
  expect(result.isError).toBeUndefined();
@@ -200,7 +209,8 @@ describe("registerSubmitReportTool", () => {
200
209
  const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
201
210
  tmpDirs.push(tmpDir);
202
211
  const outputFile = path.join(tmpDir, "report.json");
203
- const result = await handler(sampleReportParams(outputFile));
212
+ const stateFile = await writeStateFile(tmpDir, []);
213
+ const result = await handler(sampleReportParams(outputFile, stateFile));
204
214
  expect(result.isError).toBeUndefined();
205
215
  const written = JSON.parse(await fs.readFile(outputFile, "utf-8"));
206
216
  expect(written.nextSteps).toEqual([]);
@@ -209,7 +219,8 @@ describe("registerSubmitReportTool", () => {
209
219
  const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
210
220
  tmpDirs.push(tmpDir);
211
221
  const outputFile = path.join(tmpDir, "report.json");
212
- await handler(sampleReportParams(outputFile));
222
+ const stateFile = await writeStateFile(tmpDir, []);
223
+ await handler(sampleReportParams(outputFile, stateFile));
213
224
  const raw = await fs.readFile(outputFile, "utf-8");
214
225
  // Pretty-printed JSON should have indentation
215
226
  expect(raw).toContain(" ");
@@ -220,8 +231,9 @@ describe("registerSubmitReportTool", () => {
220
231
  const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
221
232
  tmpDirs.push(tmpDir);
222
233
  const outputFile = path.join(tmpDir, "report.json");
234
+ const stateFile = await writeStateFile(tmpDir, []);
223
235
  const params = {
224
- ...sampleReportParams(outputFile),
236
+ ...sampleReportParams(outputFile, stateFile),
225
237
  additionalRecommendations: [
226
238
  {
227
239
  testId: "integration-orders-patch-status",
@@ -293,6 +305,214 @@ describe("registerSubmitReportTool", () => {
293
305
  expect(result.data.primaryEndpoint).toBeUndefined();
294
306
  }
295
307
  });
308
+ // Structured grounding fields for UI recommendations.
309
+ // ----------------------------------------------------------------------
310
+ // Sub-schemas — targetElement and pageContext — populated by the agent
311
+ // from a captured browser_blueprint element. UI-only via superRefine on
312
+ // the parent newTestSchema / additionalRecommendationSchema.
313
+ describe("targetElementSchema", () => {
314
+ const validTarget = {
315
+ role: "button",
316
+ accessibleName: "Save changes",
317
+ testId: "save-changes-btn",
318
+ stableId: null,
319
+ contextText: null,
320
+ mutability: "mutable",
321
+ widgetType: "native",
322
+ };
323
+ it("accepts a fully-populated targetElement", () => {
324
+ expect(targetElementSchema.safeParse(validTarget).success).toBe(true);
325
+ });
326
+ it("accepts a minimal targetElement (only required fields)", () => {
327
+ const result = targetElementSchema.safeParse({
328
+ role: "heading",
329
+ accessibleName: "Notifications",
330
+ testId: null,
331
+ stableId: null,
332
+ contextText: null,
333
+ });
334
+ expect(result.success).toBe(true);
335
+ });
336
+ it("accepts contextText as a string array (repeating-row case)", () => {
337
+ const result = targetElementSchema.safeParse({
338
+ ...validTarget,
339
+ contextText: ["customer@example.com", "$129.99", "Pending"],
340
+ });
341
+ expect(result.success).toBe(true);
342
+ });
343
+ it("rejects targetElement missing accessibleName", () => {
344
+ const { accessibleName, ...withoutName } = validTarget;
345
+ const result = targetElementSchema.safeParse(withoutName);
346
+ expect(result.success).toBe(false);
347
+ });
348
+ it("rejects targetElement missing role", () => {
349
+ const { role, ...withoutRole } = validTarget;
350
+ const result = targetElementSchema.safeParse(withoutRole);
351
+ expect(result.success).toBe(false);
352
+ });
353
+ it("rejects an invalid mutability enum value", () => {
354
+ const result = targetElementSchema.safeParse({ ...validTarget, mutability: "sometimes" });
355
+ expect(result.success).toBe(false);
356
+ });
357
+ it("rejects an invalid widgetType enum value", () => {
358
+ const result = targetElementSchema.safeParse({ ...validTarget, widgetType: "fancy" });
359
+ expect(result.success).toBe(false);
360
+ });
361
+ });
362
+ describe("pageContextSchema (UI recommendation grounding)", () => {
363
+ it("accepts pageContext with both url and pageHash", () => {
364
+ const result = pageContextSchema.safeParse({
365
+ url: "http://localhost:5173/orders/1",
366
+ pageHash: "10:434266447",
367
+ });
368
+ expect(result.success).toBe(true);
369
+ });
370
+ it("accepts pageContext without pageHash (optional)", () => {
371
+ const result = pageContextSchema.safeParse({ url: "http://localhost:5173/orders/1" });
372
+ expect(result.success).toBe(true);
373
+ });
374
+ it("rejects pageContext missing url", () => {
375
+ const result = pageContextSchema.safeParse({ pageHash: "10:abc" });
376
+ expect(result.success).toBe(false);
377
+ });
378
+ });
379
+ describe("newTestSchema with grounding fields (UI recommendation grounding)", () => {
380
+ const validTarget = {
381
+ role: "button",
382
+ accessibleName: "Save changes",
383
+ testId: "save-changes-btn",
384
+ stableId: null,
385
+ contextText: null,
386
+ };
387
+ const validPage = {
388
+ url: "http://localhost:5173/orders/1",
389
+ pageHash: "10:434266447",
390
+ };
391
+ const baseUITest = {
392
+ testId: "ui-edit-order-discount-validation",
393
+ testType: TestType.UI,
394
+ category: "business_rule",
395
+ endpoint: "PATCH /api/v1/orders/{id}",
396
+ fileName: "edit_order_discount.spec.ts",
397
+ description: "Navigates to the Edit Order form, enters a discount > 100, and asserts that the validation error appears and the Save button is disabled.",
398
+ reasoning: "Verifies the **Save changes** button (testid `save-changes-btn`) on the Edit Order form shows the validation error when discount > 100.",
399
+ };
400
+ it("accepts a UI test with single-element targetElements + pageContext", () => {
401
+ const result = newTestSchema.safeParse({
402
+ ...baseUITest,
403
+ targetElements: [validTarget],
404
+ pageContext: validPage,
405
+ });
406
+ expect(result.success).toBe(true);
407
+ });
408
+ it("accepts a UI test with multi-element targetElements (render-state test)", () => {
409
+ const result = newTestSchema.safeParse({
410
+ ...baseUITest,
411
+ targetElements: [
412
+ {
413
+ role: "heading",
414
+ accessibleName: "Edit Order",
415
+ testId: "edit-order-heading",
416
+ stableId: null,
417
+ contextText: null,
418
+ },
419
+ {
420
+ role: "spinbutton",
421
+ accessibleName: "Discount (%)",
422
+ testId: "edit-order-input-discount",
423
+ stableId: null,
424
+ contextText: null,
425
+ },
426
+ ],
427
+ pageContext: validPage,
428
+ });
429
+ expect(result.success).toBe(true);
430
+ });
431
+ it("rejects a UI test without targetElements (required for UI grounding)", () => {
432
+ const result = newTestSchema.safeParse(baseUITest);
433
+ expect(result.success).toBe(false);
434
+ expect(JSON.stringify(result.error?.issues)).toContain("targetElements is required for testType: 'ui'");
435
+ });
436
+ it("accepts a UI test with targetElements: null (fallback case)", () => {
437
+ const result = newTestSchema.safeParse({
438
+ ...baseUITest,
439
+ targetElements: null,
440
+ description: "[no-blueprint-data] Navigates to Edit Order form and checks discount validation error.",
441
+ reasoning: "[no-blueprint-data] Verifies the discount validation error appears on the Edit Order form when discount > 100.",
442
+ });
443
+ expect(result.success).toBe(true);
444
+ });
445
+ it("rejects a contract test that includes targetElements (UI-only)", () => {
446
+ const result = newTestSchema.safeParse({
447
+ testId: "contract-orders-patch",
448
+ testType: TestType.CONTRACT,
449
+ category: "business_rule",
450
+ endpoint: "PATCH /api/v1/orders/{id}",
451
+ fileName: "orders_patch_contract.py",
452
+ reasoning: "Validates request shape and 200/422 status codes",
453
+ targetElements: [validTarget],
454
+ });
455
+ expect(result.success).toBe(false);
456
+ if (!result.success) {
457
+ expect(result.error.issues.some((i) => i.path.includes("targetElements"))).toBe(true);
458
+ }
459
+ });
460
+ it("rejects an integration test that includes pageContext (UI-only)", () => {
461
+ const result = newTestSchema.safeParse({
462
+ testId: "integration-orders",
463
+ testType: TestType.INTEGRATION,
464
+ category: "business_rule",
465
+ endpoint: "POST /api/v1/orders",
466
+ fileName: "orders_integration.py",
467
+ reasoning: "Multi-step order lifecycle",
468
+ pageContext: validPage,
469
+ });
470
+ expect(result.success).toBe(false);
471
+ if (!result.success) {
472
+ expect(result.error.issues.some((i) => i.path.includes("pageContext"))).toBe(true);
473
+ }
474
+ });
475
+ });
476
+ describe("additionalRecommendationSchema with grounding fields (UI recommendation grounding)", () => {
477
+ const validTarget = {
478
+ role: "button",
479
+ accessibleName: "Save changes",
480
+ testId: "save-changes-btn",
481
+ stableId: null,
482
+ contextText: null,
483
+ };
484
+ const validPage = { url: "http://localhost:5173/orders/1" };
485
+ it("accepts a UI rec with targetElements + pageContext", () => {
486
+ const result = additionalRecommendationSchema.safeParse({
487
+ testId: "ui-edit-order-discount-validation",
488
+ testType: TestType.UI,
489
+ category: "business_rule",
490
+ steps: [{ description: "Open Edit Order, type 150 in discount, click Save" }],
491
+ description: "Verify discount > 100 shows validation error",
492
+ priority: "medium",
493
+ reasoning: "Verify that the **Save changes** button (testid `save-changes-btn`) on the Edit Order form shows the validation error when discount > 100.",
494
+ targetElements: [validTarget],
495
+ pageContext: validPage,
496
+ });
497
+ expect(result.success).toBe(true);
498
+ });
499
+ it("rejects a non-UI rec that includes targetElements", () => {
500
+ const result = additionalRecommendationSchema.safeParse({
501
+ testId: "integration-orders-patch",
502
+ testType: TestType.INTEGRATION,
503
+ category: "business_rule",
504
+ steps: [{ method: "PATCH", path: "/api/v1/orders/1", description: "Patch order" }],
505
+ description: "Verifies PATCH workflow",
506
+ priority: "high",
507
+ reasoning: "Catches mutation bugs",
508
+ targetElements: [validTarget],
509
+ });
510
+ expect(result.success).toBe(false);
511
+ if (!result.success) {
512
+ expect(result.error.issues.some((i) => i.path.includes("targetElements"))).toBe(true);
513
+ }
514
+ });
515
+ });
296
516
  describe("analytics metrics", () => {
297
517
  beforeEach(() => {
298
518
  pushMCPToolEventMock.mockClear();
@@ -306,14 +526,15 @@ describe("registerSubmitReportTool", () => {
306
526
  const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
307
527
  tmpDirs.push(tmpDir);
308
528
  const outputFile = path.join(tmpDir, "report.json");
309
- // sampleReportParams: 1 new test, 1 maintenance (Fail→Pass), 2 results
529
+ // sampleReportParams: 1 new test, 1 maintenance (no matching stateFile entry), 2 results
310
530
  // (1 Pass + 1 Fail), 1 issue (no severity).
311
- await handler(sampleReportParams(outputFile));
531
+ const stateFile = await writeStateFile(tmpDir, []);
532
+ await handler(sampleReportParams(outputFile, stateFile));
312
533
  const params = lastAnalyticsParams();
313
534
  expect(params.testsGenerated).toBe("1");
314
535
  expect(params.testsMaintained).toBe("1");
315
536
  expect(params.recommendationsCount).toBe("0");
316
- expect(params.maintenanceRecovered).toBe("1");
537
+ expect(params.maintenanceRecovered).toBe("0"); // no matching stateFile entry → no execution data
317
538
  expect(params.testsPassed).toBe("1");
318
539
  expect(params.testsFailed).toBe("1");
319
540
  expect(params.testsSkipped).toBe("0");
@@ -323,8 +544,9 @@ describe("registerSubmitReportTool", () => {
323
544
  const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
324
545
  tmpDirs.push(tmpDir);
325
546
  const outputFile = path.join(tmpDir, "report.json");
547
+ const stateFile = await writeStateFile(tmpDir, []);
326
548
  await handler({
327
- ...sampleReportParams(outputFile),
549
+ ...sampleReportParams(outputFile, stateFile),
328
550
  issuesFound: [
329
551
  { description: "Data corruption on checkout", severity: "critical" },
330
552
  { description: "Wrong total returned", severity: "high" },
@@ -344,8 +566,10 @@ describe("registerSubmitReportTool", () => {
344
566
  const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
345
567
  tmpDirs.push(tmpDir);
346
568
  const outputFile = path.join(tmpDir, "report.json");
569
+ const stateFile = await writeStateFile(tmpDir, []);
347
570
  await handler({
348
571
  summaryOutputFile: outputFile,
572
+ stateFile,
349
573
  businessCaseAnalysis: "Config-only change.",
350
574
  newTestsCreated: [],
351
575
  testMaintenance: [],
@@ -363,36 +587,236 @@ describe("registerSubmitReportTool", () => {
363
587
  const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
364
588
  tmpDirs.push(tmpDir);
365
589
  const outputFile = path.join(tmpDir, "report.json");
590
+ const stateFile = await writeStateFile(tmpDir, []);
366
591
  await handler({
367
- ...sampleReportParams(outputFile),
592
+ ...sampleReportParams(outputFile, stateFile),
368
593
  testMaintenance: [
369
594
  // 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)" },
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: "" },
372
597
  // 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" },
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: "" },
375
600
  ],
376
601
  });
377
602
  const params = lastAnalyticsParams();
378
603
  // 2 change actions (UPDATE + REGENERATE), not 4
379
604
  expect(params.testsMaintained).toBe("2");
380
- // Both changes went Fail→Pass
381
- expect(params.maintenanceRecovered).toBe("2");
605
+ // No matching stateFile entries — execution data unavailable
606
+ expect(params.maintenanceRecovered).toBe("0");
382
607
  });
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);
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) {
386
625
  const outputFile = path.join(tmpDir, "report.json");
387
626
  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
- ],
627
+ summaryOutputFile: outputFile,
628
+ businessCaseAnalysis: "Test",
629
+ newTestsCreated: [],
630
+ testMaintenance: maintenance,
631
+ testResults: [],
632
+ issuesFound: [],
633
+ ...extra,
393
634
  });
394
- const params = lastAnalyticsParams();
395
- 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");
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: [],
396
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();
397
821
  });
398
822
  });