@skyramp/mcp 0.3.2-rc.pom-3 → 0.3.2

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 (289) hide show
  1. package/build/commands/commandLibrary.d.ts +1 -0
  2. package/build/commands/commandLibrary.js +19 -13
  3. package/build/commands/localDevTestChangesCommand.d.ts +15 -0
  4. package/build/commands/localDevTestChangesCommand.js +201 -0
  5. package/build/index.js +80 -6
  6. package/build/prompts/code-reuse.js +3 -0
  7. package/build/prompts/enhance-assertions/sharedAssertionRules.d.ts +1 -0
  8. package/build/prompts/enhance-assertions/sharedAssertionRules.js +17 -0
  9. package/build/prompts/initialize-workspace/initializeWorkspacePrompt.js +11 -9
  10. package/build/prompts/local-dev/local-dev-plan.d.ts +35 -0
  11. package/build/prompts/local-dev/local-dev-plan.js +429 -0
  12. package/build/prompts/local-dev/local-dev-prompts.d.ts +4 -0
  13. package/build/prompts/local-dev/local-dev-prompts.js +190 -0
  14. package/build/prompts/pom-aware-code-reuse.js +12 -0
  15. package/build/prompts/prompt-utils.d.ts +8 -0
  16. package/build/prompts/prompt-utils.js +33 -0
  17. package/build/prompts/sut-setup/modes/adaptWorkflowPrompt.js +37 -14
  18. package/build/prompts/sut-setup/shared.js +16 -12
  19. package/build/prompts/test-recommendation/analysisOutputPrompt.js +21 -29
  20. package/build/prompts/test-recommendation/scopeAssessment.d.ts +24 -7
  21. package/build/prompts/test-recommendation/scopeAssessment.js +111 -12
  22. package/build/prompts/test-recommendation/test-recommendation-prompt.js +41 -4
  23. package/build/prompts/testbot/testbot-prompts.d.ts +0 -5
  24. package/build/prompts/testbot/testbot-prompts.js +32 -52
  25. package/build/recommendation/planRanker.d.ts +15 -2
  26. package/build/recommendation/planRanker.js +76 -5
  27. package/build/resources/testbotResource.js +2 -1
  28. package/build/services/AnalyticsService.d.ts +1 -1
  29. package/build/services/TestExecutionService.d.ts +2 -1
  30. package/build/services/TestExecutionService.js +8 -3
  31. package/build/services/TestGenerationService.d.ts +2 -2
  32. package/build/services/TestGenerationService.js +39 -21
  33. package/build/services/containerEnv.js +3 -1
  34. package/build/tool-phases.js +6 -0
  35. package/build/tools/code-refactor/codeReuseTool.js +43 -4
  36. package/build/tools/code-refactor/enhanceAssertionsTool.js +68 -18
  37. package/build/tools/code-refactor/reuse-outcome.d.ts +109 -0
  38. package/build/tools/code-refactor/reuse-outcome.js +158 -0
  39. package/build/tools/code-refactor/reuse-state.d.ts +45 -0
  40. package/build/tools/code-refactor/reuse-state.js +140 -0
  41. package/build/tools/enrichTestWithMocksTool.d.ts +28 -0
  42. package/build/tools/enrichTestWithMocksTool.js +726 -0
  43. package/build/tools/executeSkyrampTestTool.d.ts +11 -0
  44. package/build/tools/executeSkyrampTestTool.js +62 -21
  45. package/build/tools/generate-tests/batchMockGenerationTool.d.ts +106 -0
  46. package/build/tools/generate-tests/batchMockGenerationTool.js +545 -0
  47. package/build/tools/generate-tests/generateBatchScenarioRestTool.js +25 -23
  48. package/build/tools/generate-tests/generateContractRestTool.js +2 -2
  49. package/build/tools/generate-tests/generateE2ERestTool.d.ts +1 -1
  50. package/build/tools/generate-tests/generateE2ERestTool.js +1 -1
  51. package/build/tools/generate-tests/generateLoadRestTool.d.ts +1 -1
  52. package/build/tools/generate-tests/generateLoadRestTool.js +1 -1
  53. package/build/tools/generate-tests/generateMockRestTool.d.ts +179 -6
  54. package/build/tools/generate-tests/generateMockRestTool.js +391 -22
  55. package/build/tools/generate-tests/loadTestSchema.d.ts +1 -1
  56. package/build/tools/generate-tests/planGuard.js +2 -22
  57. package/build/tools/generateEnrichedIntegrationTestTool.d.ts +25 -0
  58. package/build/tools/generateEnrichedIntegrationTestTool.js +235 -0
  59. package/build/tools/localDevWorkerComposeTool.d.ts +24 -0
  60. package/build/tools/localDevWorkerComposeTool.js +264 -0
  61. package/build/tools/one-click/oneClickTool.d.ts +13 -0
  62. package/build/tools/one-click/oneClickTool.js +195 -24
  63. package/build/tools/preflightMockCheckTool.d.ts +2 -0
  64. package/build/tools/preflightMockCheckTool.js +96 -0
  65. package/build/tools/queryProxyMocksTool.d.ts +70 -0
  66. package/build/tools/queryProxyMocksTool.js +522 -0
  67. package/build/tools/runExistingTestsTool.d.ts +31 -0
  68. package/build/tools/runExistingTestsTool.js +214 -13
  69. package/build/tools/submitReportTool.js +38 -3
  70. package/build/tools/test-management/analyzeChangesTool.d.ts +2 -1
  71. package/build/tools/test-management/analyzeChangesTool.js +63 -40
  72. package/build/tools/test-management/registerTestPlanTool.js +55 -7
  73. package/build/tools/trace/startTraceCollectionTool.js +3 -3
  74. package/build/types/FrontendIntegration.d.ts +3 -0
  75. package/build/types/FrontendIntegration.js +3 -0
  76. package/build/types/OneClickCommands.d.ts +1 -1
  77. package/build/types/Recommendation.d.ts +20 -0
  78. package/build/types/Recommendation.js +32 -6
  79. package/build/types/RepositoryAnalysis.d.ts +131 -14
  80. package/build/types/RepositoryAnalysis.js +16 -2
  81. package/build/types/ReuseOutcome.d.ts +63 -0
  82. package/build/types/ReuseOutcome.js +33 -0
  83. package/build/types/TestExecution.d.ts +1 -0
  84. package/build/types/TestTypes.d.ts +25 -7
  85. package/build/types/TestTypes.js +22 -6
  86. package/build/types/TestbotReport.d.ts +7 -0
  87. package/build/types/index.d.ts +2 -0
  88. package/build/types/index.js +1 -0
  89. package/build/utils/AnalysisStateManager.d.ts +26 -0
  90. package/build/utils/AnalysisStateManager.js +31 -1
  91. package/build/utils/analyze-openapi.js +18 -1
  92. package/build/utils/branchDiff.d.ts +17 -1
  93. package/build/utils/branchDiff.js +99 -14
  94. package/build/utils/featureFlags.d.ts +31 -0
  95. package/build/utils/featureFlags.js +37 -0
  96. package/build/utils/frontendIntegration.js +8 -1
  97. package/build/utils/grpcMockValidation.d.ts +1 -0
  98. package/build/utils/grpcMockValidation.js +49 -0
  99. package/build/utils/httpMethodValidation.d.ts +4 -0
  100. package/build/utils/httpMethodValidation.js +15 -0
  101. package/build/utils/logger.js +1 -1
  102. package/build/utils/mockCompatibility.d.ts +49 -0
  103. package/build/utils/mockCompatibility.js +82 -0
  104. package/build/utils/pom-scope/pom-files.js +7 -0
  105. package/build/utils/pom-verify/bindings.js +11 -1
  106. package/build/utils/pom-verify/re-exports.d.ts +10 -0
  107. package/build/utils/pom-verify/re-exports.js +61 -0
  108. package/build/utils/pom-verify/resolve.d.ts +4 -1
  109. package/build/utils/pom-verify/resolve.js +7 -4
  110. package/build/utils/pom-verify/verify.d.ts +5 -0
  111. package/build/utils/pom-verify/verify.js +25 -3
  112. package/build/utils/progress.js +10 -5
  113. package/build/utils/proxy-terminal.js +3 -3
  114. package/build/utils/routeParsers.d.ts +3 -9
  115. package/build/utils/routeParsers.js +79 -4
  116. package/build/utils/utils.js +2 -2
  117. package/build/utils/versions.d.ts +4 -3
  118. package/build/utils/versions.js +3 -1
  119. package/build/utils/workspaceAuth.d.ts +46 -0
  120. package/build/utils/workspaceAuth.js +156 -1
  121. package/build/workspace/testSuites.d.ts +2 -1
  122. package/build/workspace/testSuites.js +1 -1
  123. package/build/workspace/workspace.d.ts +108 -32
  124. package/build/workspace/workspace.js +32 -4
  125. package/package.json +5 -2
  126. package/build/adapters/jestAdapter.test.d.ts +0 -1
  127. package/build/adapters/jestAdapter.test.js +0 -93
  128. package/build/adapters/mochaAdapter.test.d.ts +0 -1
  129. package/build/adapters/mochaAdapter.test.js +0 -63
  130. package/build/adapters/playwrightAdapter.test.d.ts +0 -1
  131. package/build/adapters/playwrightAdapter.test.js +0 -169
  132. package/build/adapters/pytestAdapter.test.d.ts +0 -1
  133. package/build/adapters/pytestAdapter.test.js +0 -90
  134. package/build/prompts/code-reuse.test.d.ts +0 -1
  135. package/build/prompts/code-reuse.test.js +0 -62
  136. package/build/prompts/pom-aware-code-reuse.test.d.ts +0 -1
  137. package/build/prompts/pom-aware-code-reuse.test.js +0 -11
  138. package/build/prompts/test-maintenance/drift-analysis-prompt.test.d.ts +0 -1
  139. package/build/prompts/test-maintenance/drift-analysis-prompt.test.js +0 -51
  140. package/build/prompts/test-recommendation/analysisOutputPrompt.test.d.ts +0 -1
  141. package/build/prompts/test-recommendation/analysisOutputPrompt.test.js +0 -264
  142. package/build/prompts/test-recommendation/mergeEnrichedScenarios.test.d.ts +0 -1
  143. package/build/prompts/test-recommendation/mergeEnrichedScenarios.test.js +0 -126
  144. package/build/prompts/test-recommendation/promptPlan.test.d.ts +0 -1
  145. package/build/prompts/test-recommendation/promptPlan.test.js +0 -336
  146. package/build/prompts/test-recommendation/scopeAssessment.test.d.ts +0 -1
  147. package/build/prompts/test-recommendation/scopeAssessment.test.js +0 -371
  148. package/build/prompts/test-recommendation/test-recommendation-prompt.test.d.ts +0 -1
  149. package/build/prompts/test-recommendation/test-recommendation-prompt.test.js +0 -1925
  150. package/build/prompts/testbot/testbot-prompts.test.d.ts +0 -1
  151. package/build/prompts/testbot/testbot-prompts.test.js +0 -451
  152. package/build/recommendation/budgeters/diversityBalancedBudgeter.test.d.ts +0 -1
  153. package/build/recommendation/budgeters/diversityBalancedBudgeter.test.js +0 -138
  154. package/build/recommendation/budgeters/fixedNBudgeter.test.d.ts +0 -1
  155. package/build/recommendation/budgeters/fixedNBudgeter.test.js +0 -66
  156. package/build/recommendation/discriminators.test.d.ts +0 -1
  157. package/build/recommendation/discriminators.test.js +0 -324
  158. package/build/recommendation/diversity.test.d.ts +0 -1
  159. package/build/recommendation/diversity.test.js +0 -77
  160. package/build/recommendation/planRanker.test.d.ts +0 -1
  161. package/build/recommendation/planRanker.test.js +0 -110
  162. package/build/resources/testbotResource.test.d.ts +0 -1
  163. package/build/resources/testbotResource.test.js +0 -44
  164. package/build/services/AnalyticsService.test.d.ts +0 -1
  165. package/build/services/AnalyticsService.test.js +0 -86
  166. package/build/services/ScenarioGenerationService.integration.test.d.ts +0 -1
  167. package/build/services/ScenarioGenerationService.integration.test.js +0 -162
  168. package/build/services/ScenarioGenerationService.test.d.ts +0 -1
  169. package/build/services/ScenarioGenerationService.test.js +0 -414
  170. package/build/services/TestDiscoveryService.test.d.ts +0 -1
  171. package/build/services/TestDiscoveryService.test.js +0 -983
  172. package/build/services/TestExecutionService.test.d.ts +0 -1
  173. package/build/services/TestExecutionService.test.js +0 -1032
  174. package/build/services/TestGenerationService.test.d.ts +0 -1
  175. package/build/services/TestGenerationService.test.js +0 -578
  176. package/build/tool-phase-coverage.test.d.ts +0 -1
  177. package/build/tool-phase-coverage.test.js +0 -49
  178. package/build/tools/code-refactor/codeReuseTool.test.d.ts +0 -1
  179. package/build/tools/code-refactor/codeReuseTool.test.js +0 -572
  180. package/build/tools/generate-tests/generateBatchScenarioRestTool.test.d.ts +0 -1
  181. package/build/tools/generate-tests/generateBatchScenarioRestTool.test.js +0 -433
  182. package/build/tools/generate-tests/generateContractRestTool.test.d.ts +0 -1
  183. package/build/tools/generate-tests/generateContractRestTool.test.js +0 -142
  184. package/build/tools/generate-tests/generateIntegrationRestTool.test.d.ts +0 -1
  185. package/build/tools/generate-tests/generateIntegrationRestTool.test.js +0 -159
  186. package/build/tools/generate-tests/generateLoadRestTool.test.d.ts +0 -1
  187. package/build/tools/generate-tests/generateLoadRestTool.test.js +0 -169
  188. package/build/tools/generate-tests/generateUIRestTool.test.d.ts +0 -1
  189. package/build/tools/generate-tests/generateUIRestTool.test.js +0 -32
  190. package/build/tools/generate-tests/planGuard.test.d.ts +0 -1
  191. package/build/tools/generate-tests/planGuard.test.js +0 -185
  192. package/build/tools/generate-tests/scenarioLint.test.d.ts +0 -1
  193. package/build/tools/generate-tests/scenarioLint.test.js +0 -100
  194. package/build/tools/runExistingTestsTool.test.d.ts +0 -1
  195. package/build/tools/runExistingTestsTool.test.js +0 -379
  196. package/build/tools/submitReportTool.test.d.ts +0 -1
  197. package/build/tools/submitReportTool.test.js +0 -1428
  198. package/build/tools/test-management/actionsTool.test.d.ts +0 -1
  199. package/build/tools/test-management/actionsTool.test.js +0 -436
  200. package/build/tools/test-management/analyzeChangesTool.test.d.ts +0 -1
  201. package/build/tools/test-management/analyzeChangesTool.test.js +0 -522
  202. package/build/tools/test-management/analyzeTestHealthTool.test.d.ts +0 -1
  203. package/build/tools/test-management/analyzeTestHealthTool.test.js +0 -385
  204. package/build/tools/test-management/registerTestPlanTool.test.d.ts +0 -1
  205. package/build/tools/test-management/registerTestPlanTool.test.js +0 -296
  206. package/build/tools/trace/resolveSaveStoragePath.test.d.ts +0 -1
  207. package/build/tools/trace/resolveSaveStoragePath.test.js +0 -17
  208. package/build/tools/trace/resolveSessionPaths.test.d.ts +0 -1
  209. package/build/tools/trace/resolveSessionPaths.test.js +0 -104
  210. package/build/tools/trace/sessionState.test.d.ts +0 -1
  211. package/build/tools/trace/sessionState.test.js +0 -17
  212. package/build/tools/workspace/initializeWorkspaceTool.test.d.ts +0 -1
  213. package/build/tools/workspace/initializeWorkspaceTool.test.js +0 -139
  214. package/build/tools/workspace/serviceUpsert.test.d.ts +0 -1
  215. package/build/tools/workspace/serviceUpsert.test.js +0 -50
  216. package/build/utils/AnalysisStateManager.test.d.ts +0 -1
  217. package/build/utils/AnalysisStateManager.test.js +0 -134
  218. package/build/utils/dartRouteExtractor.test.d.ts +0 -1
  219. package/build/utils/dartRouteExtractor.test.js +0 -307
  220. package/build/utils/docker.test.d.ts +0 -1
  221. package/build/utils/docker.test.js +0 -114
  222. package/build/utils/featureFlags.test.d.ts +0 -1
  223. package/build/utils/featureFlags.test.js +0 -81
  224. package/build/utils/fileWalk.test.d.ts +0 -1
  225. package/build/utils/fileWalk.test.js +0 -252
  226. package/build/utils/frontendIntegration.test.d.ts +0 -1
  227. package/build/utils/frontendIntegration.test.js +0 -229
  228. package/build/utils/frontendSelectors.test.d.ts +0 -1
  229. package/build/utils/frontendSelectors.test.js +0 -118
  230. package/build/utils/gitStaging.test.d.ts +0 -1
  231. package/build/utils/gitStaging.test.js +0 -111
  232. package/build/utils/httpDefaults.test.d.ts +0 -1
  233. package/build/utils/httpDefaults.test.js +0 -21
  234. package/build/utils/importerHop.test.d.ts +0 -1
  235. package/build/utils/importerHop.test.js +0 -469
  236. package/build/utils/pathAffinityClassification.test.d.ts +0 -1
  237. package/build/utils/pathAffinityClassification.test.js +0 -208
  238. package/build/utils/planMatchKeys.test.d.ts +0 -1
  239. package/build/utils/planMatchKeys.test.js +0 -123
  240. package/build/utils/pom-scope/index.test.d.ts +0 -1
  241. package/build/utils/pom-scope/index.test.js +0 -278
  242. package/build/utils/pom-scope/pom-files.test.d.ts +0 -1
  243. package/build/utils/pom-scope/pom-files.test.js +0 -29
  244. package/build/utils/pom-scope/scoring.test.d.ts +0 -1
  245. package/build/utils/pom-scope/scoring.test.js +0 -39
  246. package/build/utils/pom-scope/selector-extractor.test.d.ts +0 -1
  247. package/build/utils/pom-scope/selector-extractor.test.js +0 -67
  248. package/build/utils/pom-verify/bindings.test.d.ts +0 -1
  249. package/build/utils/pom-verify/bindings.test.js +0 -164
  250. package/build/utils/pom-verify/calls.test.d.ts +0 -1
  251. package/build/utils/pom-verify/calls.test.js +0 -61
  252. package/build/utils/pom-verify/resolve.test.d.ts +0 -1
  253. package/build/utils/pom-verify/resolve.test.js +0 -114
  254. package/build/utils/pom-verify/verify.test.d.ts +0 -1
  255. package/build/utils/pom-verify/verify.test.js +0 -374
  256. package/build/utils/pr-comment-parser.test.d.ts +0 -1
  257. package/build/utils/pr-comment-parser.test.js +0 -428
  258. package/build/utils/progress.test.d.ts +0 -1
  259. package/build/utils/progress.test.js +0 -37
  260. package/build/utils/projectMetadata.test.d.ts +0 -1
  261. package/build/utils/projectMetadata.test.js +0 -172
  262. package/build/utils/pythonMountPrefixes.test.d.ts +0 -1
  263. package/build/utils/pythonMountPrefixes.test.js +0 -113
  264. package/build/utils/repoScanner.test.d.ts +0 -1
  265. package/build/utils/repoScanner.test.js +0 -190
  266. package/build/utils/reportVerification.test.d.ts +0 -1
  267. package/build/utils/reportVerification.test.js +0 -185
  268. package/build/utils/routeParsers.test.d.ts +0 -1
  269. package/build/utils/routeParsers.test.js +0 -1011
  270. package/build/utils/scenarioDrafting.test.d.ts +0 -1
  271. package/build/utils/scenarioDrafting.test.js +0 -876
  272. package/build/utils/sourceRouteExtractor.test.d.ts +0 -1
  273. package/build/utils/sourceRouteExtractor.test.js +0 -738
  274. package/build/utils/telemetry.test.d.ts +0 -1
  275. package/build/utils/telemetry.test.js +0 -70
  276. package/build/utils/trace-parser.test.d.ts +0 -6
  277. package/build/utils/trace-parser.test.js +0 -140
  278. package/build/utils/uiPageEnumerator.test.d.ts +0 -1
  279. package/build/utils/uiPageEnumerator.test.js +0 -824
  280. package/build/utils/utils.test.d.ts +0 -1
  281. package/build/utils/utils.test.js +0 -103
  282. package/build/utils/walkerCharacterization.test.d.ts +0 -1
  283. package/build/utils/walkerCharacterization.test.js +0 -233
  284. package/build/utils/workspaceAuth.test.d.ts +0 -1
  285. package/build/utils/workspaceAuth.test.js +0 -260
  286. package/build/workspace/testSuites.test.d.ts +0 -1
  287. package/build/workspace/testSuites.test.js +0 -23
  288. package/build/workspace/workspace.test.d.ts +0 -1
  289. package/build/workspace/workspace.test.js +0 -264
@@ -1,1428 +0,0 @@
1
- // @ts-nocheck - Jest ESM type inference issues
2
- import { jest } from "@jest/globals";
3
- import { TestType } from "../types/TestTypes.js";
4
- import { DriftAction } from "../types/TestAnalysis.js";
5
- import { TestExecutionStatus } from "../types/TestExecution.js";
6
- import * as fs from "fs/promises";
7
- import * as path from "path";
8
- import * as os from "os";
9
- import { execFileSync } from "child_process";
10
- import { z } from "zod";
11
- jest.unstable_mockModule("../utils/logger.js", () => ({
12
- logger: { info: jest.fn(), error: jest.fn(), warning: jest.fn(), debug: jest.fn() },
13
- }));
14
- jest.unstable_mockModule("../services/AnalyticsService.js", () => ({
15
- AnalyticsService: { pushMCPToolEvent: jest.fn().mockResolvedValue(undefined) },
16
- }));
17
- // @ts-ignore
18
- const { registerSubmitReportTool, additionalRecommendationSchema, newTestSchema, targetElementSchema, pageContextSchema } = await import("./submitReportTool.js");
19
- const { AnalyticsService } = await import("../services/AnalyticsService.js");
20
- const { CandidateSource } = await import("../types/Recommendation.js");
21
- const pushMCPToolEventMock = AnalyticsService.pushMCPToolEvent;
22
- function captureToolHandler() {
23
- let handler;
24
- let inputSchema;
25
- const fakeServer = {
26
- registerTool: (_name, opts, fn) => {
27
- handler = fn;
28
- inputSchema = opts.inputSchema;
29
- },
30
- };
31
- registerSubmitReportTool(fakeServer);
32
- // Mirrors the real MCP SDK's registerTool: validates args against inputSchema via Zod
33
- // before invoking the handler, so tests exercise the real tool contract (refinements
34
- // included) instead of bypassing validation entirely.
35
- return async (params) => {
36
- const parsed = z.object(inputSchema).safeParse(params);
37
- if (!parsed.success) {
38
- return { isError: true, content: [{ type: "text", text: parsed.error.message }] };
39
- }
40
- return handler(parsed.data);
41
- };
42
- }
43
- async function writeStateFile(dir, existingTests) {
44
- const stateFile = path.join(dir, "analyze-changes-state.json");
45
- // maintenanceVerdicts: [] simulates skyramp_actions having already run with nothing to
46
- // do — the realistic state for tests not specifically exercising the maintenance flow.
47
- await fs.writeFile(stateFile, JSON.stringify({ existingTests, maintenanceVerdicts: [], metadata: sampleMetadata(dir) }), "utf-8");
48
- return stateFile;
49
- }
50
- function sampleMetadata(dir) {
51
- return {
52
- sessionId: "test-session",
53
- stateType: "unified",
54
- repositoryPath: dir,
55
- createdAt: "2026-01-01T00:00:00.000Z",
56
- updatedAt: "2026-01-01T00:00:00.000Z",
57
- };
58
- }
59
- // SKYR-3906: state written by skyramp_actions after drift-analysis reconciliation.
60
- async function writeReconciledStateFile(dir, existingTests, maintenanceVerdicts, ignoredTestFiles = []) {
61
- const stateFile = path.join(dir, "analyze-changes-state.json");
62
- await fs.writeFile(stateFile, JSON.stringify({ existingTests, maintenanceVerdicts, ignoredTestFiles, metadata: sampleMetadata(dir) }), "utf-8");
63
- return stateFile;
64
- }
65
- function sampleReportParams(outputFile, stateFile) {
66
- return {
67
- summaryOutputFile: outputFile,
68
- stateFile,
69
- businessCaseAnalysis: "This PR adds product search. Tests needed for filtering.",
70
- newTestsCreated: [
71
- { testId: "smoke-get-products-search", testType: TestType.SMOKE, category: "workflow", endpoint: "GET /api/v1/products/search", fileName: "search_smoke_test.py", description: "Searches products by category filter and asserts matching results are returned", reasoning: "Validates product search returns correct results for category filter" },
72
- ],
73
- testResults: [
74
- { testType: TestType.SMOKE, endpoint: "GET /api/v1/products", status: "Pass", details: "2.1s, products_smoke_test.py" },
75
- { testType: TestType.SMOKE, endpoint: "GET /api/v1/products/search", status: "Fail", details: "3.4s, search_smoke_test.py" },
76
- ],
77
- issuesFound: [{ description: "Search endpoint returns 500 with category filter" }],
78
- };
79
- }
80
- describe("registerSubmitReportTool", () => {
81
- let handler;
82
- let tmpDirs = [];
83
- beforeAll(() => {
84
- handler = captureToolHandler();
85
- });
86
- afterEach(async () => {
87
- for (const dir of tmpDirs) {
88
- await fs.rm(dir, { recursive: true, force: true });
89
- }
90
- tmpDirs = [];
91
- });
92
- it("should register the tool on the server with nextSteps in schema", () => {
93
- const registerToolMock = jest.fn();
94
- const fakeServer = { registerTool: registerToolMock };
95
- registerSubmitReportTool(fakeServer);
96
- expect(registerToolMock).toHaveBeenCalledWith("skyramp_submit_report", expect.objectContaining({
97
- description: expect.any(String),
98
- inputSchema: expect.objectContaining({
99
- nextSteps: expect.any(Object),
100
- }),
101
- }), expect.any(Function));
102
- });
103
- it("should write JSON report to the specified file", async () => {
104
- const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
105
- tmpDirs.push(tmpDir);
106
- const outputFile = path.join(tmpDir, "report.json");
107
- const stateFile = await writeStateFile(tmpDir, []);
108
- const result = await handler(sampleReportParams(outputFile, stateFile));
109
- expect(result.isError).toBeUndefined();
110
- expect(result.content[0].text).toContain("Report submitted successfully");
111
- const written = JSON.parse(await fs.readFile(outputFile, "utf-8"));
112
- expect(written.businessCaseAnalysis).toBe("This PR adds product search. Tests needed for filtering.");
113
- expect(written.newTestsCreated).toHaveLength(1);
114
- expect(written.testResults).toHaveLength(2);
115
- expect(written.issuesFound).toHaveLength(1);
116
- expect(written.testMaintenance).toBeUndefined();
117
- });
118
- it("accepts and persists the per-item `repository` field", async () => {
119
- const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
120
- tmpDirs.push(tmpDir);
121
- const outputFile = path.join(tmpDir, "report.json");
122
- const stateFile = await writeStateFile(tmpDir, []);
123
- const params = sampleReportParams(outputFile, stateFile);
124
- // Attribute one item to a related repo, leave others on the primary.
125
- params.newTestsCreated[0] = { ...params.newTestsCreated[0], repository: "acme/backend" };
126
- params.testResults[0] = { ...params.testResults[0], repository: "acme/backend" };
127
- params.issuesFound[0] = { ...params.issuesFound[0], repository: "acme/backend" };
128
- const result = await handler(params);
129
- expect(result.isError).toBeUndefined();
130
- const written = JSON.parse(await fs.readFile(outputFile, "utf-8"));
131
- expect(written.newTestsCreated[0].repository).toBe("acme/backend");
132
- expect(written.testResults[0].repository).toBe("acme/backend");
133
- expect(written.issuesFound[0].repository).toBe("acme/backend");
134
- // Items without a repository stay primary (undefined → omitted from JSON).
135
- expect(written.testResults[1].repository).toBeUndefined();
136
- });
137
- it("normalizes a blank/whitespace-only `repository` to undefined", async () => {
138
- const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
139
- tmpDirs.push(tmpDir);
140
- const outputFile = path.join(tmpDir, "report.json");
141
- const stateFile = await writeStateFile(tmpDir, []);
142
- const params = sampleReportParams(outputFile, stateFile);
143
- params.testResults[0] = { ...params.testResults[0], repository: " " };
144
- const result = await handler(params);
145
- expect(result.isError).toBeUndefined();
146
- const written = JSON.parse(await fs.readFile(outputFile, "utf-8"));
147
- // Blank string must not serialize as a distinct empty repo.
148
- expect(written.testResults[0].repository).toBeUndefined();
149
- });
150
- it("should create parent directories if they don't exist", async () => {
151
- const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
152
- tmpDirs.push(tmpDir);
153
- const outputFile = path.join(tmpDir, "nested", "dirs", "report.json");
154
- const stateFile = await writeStateFile(tmpDir, []);
155
- const result = await handler(sampleReportParams(outputFile, stateFile));
156
- expect(result.isError).toBeUndefined();
157
- const written = JSON.parse(await fs.readFile(outputFile, "utf-8"));
158
- expect(written.businessCaseAnalysis).toBeDefined();
159
- });
160
- it("should handle empty arrays for optional sections", async () => {
161
- const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
162
- tmpDirs.push(tmpDir);
163
- const outputFile = path.join(tmpDir, "report.json");
164
- const stateFile = await writeStateFile(tmpDir, []);
165
- const result = await handler({
166
- summaryOutputFile: outputFile,
167
- stateFile,
168
- businessCaseAnalysis: "Simple config change.",
169
- newTestsCreated: [],
170
- testResults: [
171
- { testType: TestType.SMOKE, endpoint: "GET /api/v1/products", status: "Pass", details: "1.0s, test.py" },
172
- ],
173
- issuesFound: [],
174
- });
175
- expect(result.isError).toBeUndefined();
176
- const written = JSON.parse(await fs.readFile(outputFile, "utf-8"));
177
- expect(written.newTestsCreated).toEqual([]);
178
- expect(written.testMaintenance).toBeUndefined();
179
- expect(written.issuesFound).toEqual([]);
180
- expect(written.testResults).toHaveLength(1);
181
- });
182
- it("should return error for invalid file path", async () => {
183
- const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
184
- tmpDirs.push(tmpDir);
185
- const stateFile = await writeStateFile(tmpDir, []);
186
- const result = await handler(sampleReportParams("/nonexistent/readonly/path/report.json", stateFile));
187
- expect(result.isError).toBe(true);
188
- expect(result.content[0].text).toContain("Failed to write report");
189
- });
190
- it("should write commitMessage when provided", async () => {
191
- const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
192
- tmpDirs.push(tmpDir);
193
- const outputFile = path.join(tmpDir, "report.json");
194
- const stateFile = await writeStateFile(tmpDir, []);
195
- const result = await handler({
196
- ...sampleReportParams(outputFile, stateFile),
197
- commitMessage: "add contract tests for /products endpoint",
198
- });
199
- expect(result.isError).toBeUndefined();
200
- const written = JSON.parse(await fs.readFile(outputFile, "utf-8"));
201
- expect(written.commitMessage).toBe("add contract tests for /products endpoint");
202
- });
203
- it("should use default commitMessage when omitted", async () => {
204
- const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
205
- tmpDirs.push(tmpDir);
206
- const outputFile = path.join(tmpDir, "report.json");
207
- const stateFile = await writeStateFile(tmpDir, []);
208
- const result = await handler(sampleReportParams(outputFile, stateFile));
209
- expect(result.isError).toBeUndefined();
210
- const written = JSON.parse(await fs.readFile(outputFile, "utf-8"));
211
- expect(written.commitMessage).toBe("Added recommendations by Skyramp Testbot.");
212
- });
213
- it("should sanitize commitMessage (collapse newlines, trim whitespace)", async () => {
214
- const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
215
- tmpDirs.push(tmpDir);
216
- const outputFile = path.join(tmpDir, "report.json");
217
- const stateFile = await writeStateFile(tmpDir, []);
218
- const result = await handler({
219
- ...sampleReportParams(outputFile, stateFile),
220
- commitMessage: " line one\nline two\r\nline three ",
221
- });
222
- expect(result.isError).toBeUndefined();
223
- const written = JSON.parse(await fs.readFile(outputFile, "utf-8"));
224
- expect(written.commitMessage).toBe("line one line two line three");
225
- // No length cap — truncation is the consumer's responsibility (SKYR-3757).
226
- });
227
- it("should preserve commitMessage longer than 72 chars without truncation", async () => {
228
- const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
229
- tmpDirs.push(tmpDir);
230
- const outputFile = path.join(tmpDir, "report.json");
231
- const stateFile = await writeStateFile(tmpDir, []);
232
- const longMessage = "add contract and integration tests for the new PATCH /orders/{order_id} endpoint including discount recalculation and line-item edits";
233
- const result = await handler({
234
- ...sampleReportParams(outputFile, stateFile),
235
- commitMessage: longMessage,
236
- });
237
- expect(result.isError).toBeUndefined();
238
- const written = JSON.parse(await fs.readFile(outputFile, "utf-8"));
239
- expect(written.commitMessage).toBe(longMessage);
240
- expect(written.commitMessage.length).toBeGreaterThan(72);
241
- });
242
- it("should use default commitMessage when whitespace-only", async () => {
243
- const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
244
- tmpDirs.push(tmpDir);
245
- const outputFile = path.join(tmpDir, "report.json");
246
- const stateFile = await writeStateFile(tmpDir, []);
247
- const result = await handler({
248
- ...sampleReportParams(outputFile, stateFile),
249
- commitMessage: " \n\r\n ",
250
- });
251
- expect(result.isError).toBeUndefined();
252
- const written = JSON.parse(await fs.readFile(outputFile, "utf-8"));
253
- expect(written.commitMessage).toBe("Added recommendations by Skyramp Testbot.");
254
- });
255
- it("includes nextSteps array in written JSON", async () => {
256
- const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
257
- tmpDirs.push(tmpDir);
258
- const outputFile = path.join(tmpDir, "report.json");
259
- const stateFile = await writeStateFile(tmpDir, []);
260
- const result = await handler({
261
- ...sampleReportParams(outputFile, stateFile),
262
- nextSteps: ["Check your targetSetupCommand — endpoints returned 404"],
263
- });
264
- expect(result.isError).toBeUndefined();
265
- const written = JSON.parse(await fs.readFile(outputFile, "utf-8"));
266
- expect(written.nextSteps).toEqual([
267
- "Check your targetSetupCommand — endpoints returned 404",
268
- ]);
269
- });
270
- it("defaults nextSteps to empty array when omitted", async () => {
271
- const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
272
- tmpDirs.push(tmpDir);
273
- const outputFile = path.join(tmpDir, "report.json");
274
- const stateFile = await writeStateFile(tmpDir, []);
275
- const result = await handler(sampleReportParams(outputFile, stateFile));
276
- expect(result.isError).toBeUndefined();
277
- const written = JSON.parse(await fs.readFile(outputFile, "utf-8"));
278
- expect(written.nextSteps).toEqual([]);
279
- });
280
- it("should produce valid JSON with pretty formatting", async () => {
281
- const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
282
- tmpDirs.push(tmpDir);
283
- const outputFile = path.join(tmpDir, "report.json");
284
- const stateFile = await writeStateFile(tmpDir, []);
285
- await handler(sampleReportParams(outputFile, stateFile));
286
- const raw = await fs.readFile(outputFile, "utf-8");
287
- // Pretty-printed JSON should have indentation
288
- expect(raw).toContain(" ");
289
- // Should not contain summaryOutputFile in the output
290
- expect(raw).not.toContain("summaryOutputFile");
291
- });
292
- it("additionalRecommendations serialises primaryEndpoint into the report JSON", async () => {
293
- const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
294
- tmpDirs.push(tmpDir);
295
- const outputFile = path.join(tmpDir, "report.json");
296
- const stateFile = await writeStateFile(tmpDir, []);
297
- const params = {
298
- ...sampleReportParams(outputFile, stateFile),
299
- additionalRecommendations: [
300
- {
301
- testId: "integration-orders-patch-status",
302
- testType: TestType.INTEGRATION,
303
- category: "business_rule",
304
- primaryEndpoint: "PATCH /api/v1/orders/{order_id}",
305
- scenarioName: "orders-patch-status-transition",
306
- steps: [{ method: "PATCH", path: "/api/v1/orders/1", description: "Patch order status" }],
307
- description: "Verifies status transitions persist across PATCH calls",
308
- priority: "high",
309
- reasoning: "Catches broken state machine transitions on the new PATCH endpoint",
310
- },
311
- ],
312
- };
313
- const result = await handler(params);
314
- expect(result.isError).toBeUndefined();
315
- const written = JSON.parse(await fs.readFile(outputFile, "utf-8"));
316
- expect(written.additionalRecommendations[0].primaryEndpoint).toBe("PATCH /api/v1/orders/{order_id}");
317
- });
318
- it("additionalRecommendationSchema rejects a contract test missing primaryEndpoint", () => {
319
- const result = additionalRecommendationSchema.safeParse({
320
- testId: "contract-orders-patch-404",
321
- testType: TestType.CONTRACT,
322
- category: "error_handling",
323
- // primaryEndpoint intentionally omitted — required for contract tests
324
- scenarioName: "orders-patch-not-found",
325
- steps: [{ method: "PATCH", path: "/api/v1/orders/99999", description: "Patch non-existent order" }],
326
- description: "Verifies 404 for unknown order_id",
327
- priority: "high",
328
- reasoning: "Validates the 404 guard on the new PATCH endpoint",
329
- });
330
- expect(result.success).toBe(false);
331
- const issue = result.error?.issues.find(i => i.path.includes("primaryEndpoint"));
332
- expect(issue).toBeDefined();
333
- });
334
- it("additionalRecommendationSchema accepts a contract test with primaryEndpoint", () => {
335
- const result = additionalRecommendationSchema.safeParse({
336
- testId: "contract-orders-patch-404",
337
- testType: TestType.CONTRACT,
338
- category: "error_handling",
339
- primaryEndpoint: "PATCH /api/v1/orders/{order_id}",
340
- scenarioName: "orders-patch-not-found",
341
- steps: [{ method: "PATCH", path: "/api/v1/orders/99999", description: "Patch non-existent order" }],
342
- description: "Verifies 404 for unknown order_id",
343
- priority: "high",
344
- reasoning: "Validates the 404 guard on the new PATCH endpoint",
345
- });
346
- expect(result.success).toBe(true);
347
- });
348
- it("additionalRecommendationSchema accepts a multi-step integration test without primaryEndpoint", () => {
349
- // For multi-step scenarios, primaryEndpoint is optional — steps is the authoritative source
350
- const result = additionalRecommendationSchema.safeParse({
351
- testId: "integration-orders-patch-mutation",
352
- testType: TestType.INTEGRATION,
353
- category: "business_rule",
354
- // primaryEndpoint intentionally omitted for multi-step scenario
355
- scenarioName: "orders-patch-mutation-verify",
356
- steps: [
357
- { method: "POST", path: "/api/v1/products", description: "Create product" },
358
- { method: "POST", path: "/api/v1/orders", description: "Create order" },
359
- { method: "PATCH", path: "/api/v1/orders/1", description: "Patch order" },
360
- ],
361
- description: "Verifies total_amount recalculates after PATCH",
362
- priority: "high",
363
- reasoning: "Catches item accumulation bug",
364
- });
365
- expect(result.success).toBe(true);
366
- if (result.success) {
367
- expect(result.data.primaryEndpoint).toBeUndefined();
368
- }
369
- });
370
- // Structured grounding fields for UI recommendations.
371
- // ----------------------------------------------------------------------
372
- // Sub-schemas — targetElement and pageContext — populated by the agent
373
- // from a captured browser_blueprint element. UI-only via superRefine on
374
- // the parent newTestSchema / additionalRecommendationSchema.
375
- describe("targetElementSchema", () => {
376
- const validTarget = {
377
- role: "button",
378
- accessibleName: "Save changes",
379
- testId: "save-changes-btn",
380
- stableId: null,
381
- contextText: null,
382
- mutability: "mutable",
383
- widgetType: "native",
384
- };
385
- it("accepts a fully-populated targetElement", () => {
386
- expect(targetElementSchema.safeParse(validTarget).success).toBe(true);
387
- });
388
- it("accepts a minimal targetElement (only required fields)", () => {
389
- const result = targetElementSchema.safeParse({
390
- role: "heading",
391
- accessibleName: "Notifications",
392
- testId: null,
393
- stableId: null,
394
- contextText: null,
395
- });
396
- expect(result.success).toBe(true);
397
- });
398
- it("accepts contextText as a string array (repeating-row case)", () => {
399
- const result = targetElementSchema.safeParse({
400
- ...validTarget,
401
- contextText: ["customer@example.com", "$129.99", "Pending"],
402
- });
403
- expect(result.success).toBe(true);
404
- });
405
- it("rejects targetElement missing accessibleName", () => {
406
- const { accessibleName: _accessibleName, ...withoutName } = validTarget;
407
- const result = targetElementSchema.safeParse(withoutName);
408
- expect(result.success).toBe(false);
409
- });
410
- it("rejects targetElement missing role", () => {
411
- const { role: _role, ...withoutRole } = validTarget;
412
- const result = targetElementSchema.safeParse(withoutRole);
413
- expect(result.success).toBe(false);
414
- });
415
- it("rejects an invalid mutability enum value", () => {
416
- const result = targetElementSchema.safeParse({ ...validTarget, mutability: "sometimes" });
417
- expect(result.success).toBe(false);
418
- });
419
- it("rejects an invalid widgetType enum value", () => {
420
- const result = targetElementSchema.safeParse({ ...validTarget, widgetType: "fancy" });
421
- expect(result.success).toBe(false);
422
- });
423
- });
424
- describe("pageContextSchema (UI recommendation grounding)", () => {
425
- it("accepts pageContext with both url and pageHash", () => {
426
- const result = pageContextSchema.safeParse({
427
- url: "http://localhost:5173/orders/1",
428
- pageHash: "10:434266447",
429
- });
430
- expect(result.success).toBe(true);
431
- });
432
- it("accepts pageContext without pageHash (optional)", () => {
433
- const result = pageContextSchema.safeParse({ url: "http://localhost:5173/orders/1" });
434
- expect(result.success).toBe(true);
435
- });
436
- it("rejects pageContext missing url", () => {
437
- const result = pageContextSchema.safeParse({ pageHash: "10:abc" });
438
- expect(result.success).toBe(false);
439
- });
440
- });
441
- describe("newTestSchema with grounding fields (UI recommendation grounding)", () => {
442
- const validTarget = {
443
- role: "button",
444
- accessibleName: "Save changes",
445
- testId: "save-changes-btn",
446
- stableId: null,
447
- contextText: null,
448
- };
449
- const validPage = {
450
- url: "http://localhost:5173/orders/1",
451
- pageHash: "10:434266447",
452
- };
453
- const baseUITest = {
454
- testId: "ui-edit-order-discount-validation",
455
- testType: TestType.UI,
456
- category: "business_rule",
457
- endpoint: "PATCH /api/v1/orders/{id}",
458
- fileName: "edit_order_discount.spec.ts",
459
- description: "Navigates to the Edit Order form, enters a discount > 100, and asserts that the validation error appears and the Save button is disabled.",
460
- reasoning: "Verifies the **Save changes** button (testid `save-changes-btn`) on the Edit Order form shows the validation error when discount > 100.",
461
- };
462
- it("accepts a UI test with single-element targetElements + pageContext", () => {
463
- const result = newTestSchema.safeParse({
464
- ...baseUITest,
465
- targetElements: [validTarget],
466
- pageContext: validPage,
467
- });
468
- expect(result.success).toBe(true);
469
- });
470
- it("accepts a UI test with multi-element targetElements (render-state test)", () => {
471
- const result = newTestSchema.safeParse({
472
- ...baseUITest,
473
- targetElements: [
474
- {
475
- role: "heading",
476
- accessibleName: "Edit Order",
477
- testId: "edit-order-heading",
478
- stableId: null,
479
- contextText: null,
480
- },
481
- {
482
- role: "spinbutton",
483
- accessibleName: "Discount (%)",
484
- testId: "edit-order-input-discount",
485
- stableId: null,
486
- contextText: null,
487
- },
488
- ],
489
- pageContext: validPage,
490
- });
491
- expect(result.success).toBe(true);
492
- });
493
- it("rejects a UI test without targetElements (required for UI grounding)", () => {
494
- const result = newTestSchema.safeParse(baseUITest);
495
- expect(result.success).toBe(false);
496
- expect(JSON.stringify(result.error?.issues)).toContain("targetElements is required for testType: 'ui'");
497
- });
498
- it("accepts a UI test with targetElements: null (fallback case)", () => {
499
- const result = newTestSchema.safeParse({
500
- ...baseUITest,
501
- targetElements: null,
502
- description: "[no-blueprint-data] Navigates to Edit Order form and checks discount validation error.",
503
- reasoning: "[no-blueprint-data] Verifies the discount validation error appears on the Edit Order form when discount > 100.",
504
- });
505
- expect(result.success).toBe(true);
506
- });
507
- it("rejects a contract test that includes targetElements (UI-only)", () => {
508
- const result = newTestSchema.safeParse({
509
- testId: "contract-orders-patch",
510
- testType: TestType.CONTRACT,
511
- category: "business_rule",
512
- endpoint: "PATCH /api/v1/orders/{id}",
513
- fileName: "orders_patch_contract.py",
514
- reasoning: "Validates request shape and 200/422 status codes",
515
- targetElements: [validTarget],
516
- description: "calls the PATCH /api/v1/orders/{id} API and validates it implements its contract.",
517
- });
518
- expect(result.success).toBe(false);
519
- if (!result.success) {
520
- expect(result.error.issues.some((i) => i.path.includes("targetElements"))).toBe(true);
521
- }
522
- });
523
- it("rejects an integration test that includes pageContext (UI-only)", () => {
524
- const result = newTestSchema.safeParse({
525
- testId: "integration-orders",
526
- testType: TestType.INTEGRATION,
527
- category: "business_rule",
528
- endpoint: "POST /api/v1/orders",
529
- fileName: "orders_integration.py",
530
- reasoning: "Multi-step order lifecycle",
531
- pageContext: validPage,
532
- description: "calls the POST /api/v1/orders API to create an order and then do stuff.",
533
- });
534
- expect(result.success).toBe(false);
535
- if (!result.success) {
536
- expect(result.error.issues.some((i) => i.path.includes("pageContext"))).toBe(true);
537
- }
538
- });
539
- });
540
- describe("additionalRecommendationSchema with grounding fields (UI recommendation grounding)", () => {
541
- const validTarget = {
542
- role: "button",
543
- accessibleName: "Save changes",
544
- testId: "save-changes-btn",
545
- stableId: null,
546
- contextText: null,
547
- };
548
- const validPage = { url: "http://localhost:5173/orders/1" };
549
- it("accepts a UI rec with targetElements + pageContext", () => {
550
- const result = additionalRecommendationSchema.safeParse({
551
- testId: "ui-edit-order-discount-validation",
552
- testType: TestType.UI,
553
- category: "business_rule",
554
- steps: [{ description: "Open Edit Order, type 150 in discount, click Save" }],
555
- description: "Verify discount > 100 shows validation error",
556
- priority: "medium",
557
- reasoning: "Verify that the **Save changes** button (testid `save-changes-btn`) on the Edit Order form shows the validation error when discount > 100.",
558
- targetElements: [validTarget],
559
- pageContext: validPage,
560
- });
561
- expect(result.success).toBe(true);
562
- });
563
- it("rejects a non-UI rec that includes targetElements", () => {
564
- const result = additionalRecommendationSchema.safeParse({
565
- testId: "integration-orders-patch",
566
- testType: TestType.INTEGRATION,
567
- category: "business_rule",
568
- steps: [{ method: "PATCH", path: "/api/v1/orders/1", description: "Patch order" }],
569
- description: "Verifies PATCH workflow",
570
- priority: "high",
571
- reasoning: "Catches mutation bugs",
572
- targetElements: [validTarget],
573
- });
574
- expect(result.success).toBe(false);
575
- if (!result.success) {
576
- expect(result.error.issues.some((i) => i.path.includes("targetElements"))).toBe(true);
577
- }
578
- });
579
- });
580
- describe("analytics metrics", () => {
581
- beforeEach(() => {
582
- pushMCPToolEventMock.mockClear();
583
- });
584
- function lastAnalyticsParams() {
585
- expect(pushMCPToolEventMock).toHaveBeenCalled();
586
- const calls = pushMCPToolEventMock.mock.calls;
587
- return calls[calls.length - 1][2];
588
- }
589
- it("emits per-run count metrics derived from the report", async () => {
590
- const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
591
- tmpDirs.push(tmpDir);
592
- const outputFile = path.join(tmpDir, "report.json");
593
- // sampleReportParams: 1 new test, 2 results (1 Pass + 1 Fail), 1 issue (no severity).
594
- // Plus 1 maintenance verdict (no matching existingTests entry) from state.
595
- const stateFile = await writeReconciledStateFile(tmpDir, [], [
596
- { testFilePath: "/repo/products_contract_test.py", action: DriftAction.Update, rationale: "Updated auth flow in existing tests", testType: TestType.CONTRACT, endpoint: "GET /api/v1/products" },
597
- ]);
598
- await handler(sampleReportParams(outputFile, stateFile));
599
- const params = lastAnalyticsParams();
600
- expect(params.testsGenerated).toBe("1");
601
- expect(params.testsMaintained).toBe("1");
602
- expect(params.recommendationsCount).toBe("0");
603
- expect(params.maintenanceRecovered).toBe("0"); // no matching stateFile entry → no execution data
604
- expect(params.testsPassed).toBe("1");
605
- expect(params.testsFailed).toBe("1");
606
- expect(params.testsSkipped).toBe("0");
607
- expect(params.issuesFound).toBe("1");
608
- });
609
- it("breaks issues down by severity", async () => {
610
- const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
611
- tmpDirs.push(tmpDir);
612
- const outputFile = path.join(tmpDir, "report.json");
613
- const stateFile = await writeStateFile(tmpDir, []);
614
- await handler({
615
- ...sampleReportParams(outputFile, stateFile),
616
- issuesFound: [
617
- { description: "Data corruption on checkout", severity: "critical" },
618
- { description: "Wrong total returned", severity: "high" },
619
- { description: "Another wrong total", severity: "high" },
620
- { description: "Minor gap", severity: "medium" },
621
- { description: "Unlabeled issue" },
622
- ],
623
- });
624
- const params = lastAnalyticsParams();
625
- expect(params.issuesFound).toBe("5");
626
- expect(params.issuesCritical).toBe("1");
627
- expect(params.issuesHigh).toBe("2");
628
- expect(params.issuesMedium).toBe("1");
629
- expect(params.issuesLow).toBe("0");
630
- });
631
- it("reports zero metrics for an empty report", async () => {
632
- const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
633
- tmpDirs.push(tmpDir);
634
- const outputFile = path.join(tmpDir, "report.json");
635
- const stateFile = await writeStateFile(tmpDir, []);
636
- await handler({
637
- summaryOutputFile: outputFile,
638
- stateFile,
639
- businessCaseAnalysis: "Config-only change.",
640
- newTestsCreated: [],
641
- testResults: [],
642
- issuesFound: [],
643
- });
644
- const params = lastAnalyticsParams();
645
- expect(params.testsGenerated).toBe("0");
646
- expect(params.testsMaintained).toBe("0");
647
- expect(params.maintenanceRecovered).toBe("0");
648
- expect(params.testsPassed).toBe("0");
649
- expect(params.issuesFound).toBe("0");
650
- });
651
- it("testsMaintained counts only change actions, not VERIFY/IGNORE no-ops", async () => {
652
- const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
653
- tmpDirs.push(tmpDir);
654
- const outputFile = path.join(tmpDir, "report.json");
655
- const stateFile = await writeReconciledStateFile(tmpDir, [], [
656
- // Real change
657
- { testFilePath: "/repo/products_contract_test.py", action: DriftAction.Update, rationale: "Patched auth", testType: TestType.CONTRACT, endpoint: "GET /api/v1/products" },
658
- { testFilePath: "/repo/orders_smoke_test.py", action: DriftAction.Regenerate, rationale: "Rewrote for new shape", testType: TestType.SMOKE, endpoint: "GET /api/v1/orders" },
659
- // No-ops — assessed but not modified
660
- { testFilePath: "/repo/reviews_smoke_test.py", action: DriftAction.Ignore, rationale: "No action required", testType: TestType.SMOKE, endpoint: "GET /api/v1/reviews" },
661
- { testFilePath: "/repo/coupons_smoke_test.py", action: DriftAction.Verify, rationale: "Verified still valid", testType: TestType.SMOKE, endpoint: "GET /api/v1/coupons" },
662
- ]);
663
- await handler(sampleReportParams(outputFile, stateFile));
664
- const params = lastAnalyticsParams();
665
- // 2 change actions (UPDATE + REGENERATE), not 4
666
- expect(params.testsMaintained).toBe("2");
667
- // No matching stateFile entries — execution data unavailable
668
- expect(params.maintenanceRecovered).toBe("0");
669
- });
670
- });
671
- // ─────────────────────────────────────────────────────────────────────────────
672
- // testMaintenance output shapes
673
- // Each case documents exactly what the MCP writes to JSON, which is what the
674
- // testbot renderer reads. Keeping these in sync prevents silent regressions
675
- // when toOutput or the stateFile enrichment path changes.
676
- // ─────────────────────────────────────────────────────────────────────────────
677
- describe("testMaintenance output shapes", () => {
678
- const baseVerdict = {
679
- testFilePath: "/repo/backend/tests/orders_contract_test.py",
680
- rationale: "Updated discount fields in assertions",
681
- testType: TestType.CONTRACT,
682
- endpoint: "GET /api/v1/orders",
683
- };
684
- async function submitAndReadMaintenance(tmpDir, stateFile, testMaintenanceDetails = []) {
685
- const outputFile = path.join(tmpDir, "report.json");
686
- const result = await handler({
687
- summaryOutputFile: outputFile,
688
- stateFile,
689
- businessCaseAnalysis: "Test",
690
- newTestsCreated: [],
691
- testMaintenanceDetails,
692
- testResults: [],
693
- issuesFound: [],
694
- });
695
- if (result.isError)
696
- throw new Error(`handler rejected: ${result.content?.[0]?.text}`);
697
- const written = JSON.parse(await fs.readFile(outputFile, "utf-8"));
698
- return written.testMaintenance;
699
- }
700
- // ── No matching existingTests entry ───────────────────────────────────────
701
- it("UPDATE with no matching existingTests entry: beforeStatus=Unknown, afterStatus=Unknown", async () => {
702
- const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
703
- tmpDirs.push(tmpDir);
704
- const stateFile = await writeReconciledStateFile(tmpDir, [], [{ ...baseVerdict, action: DriftAction.Update }]);
705
- const result = await submitAndReadMaintenance(tmpDir, stateFile);
706
- expect(result[0].beforeStatus).toBe("Unknown");
707
- expect(result[0].afterStatus).toBe("Unknown");
708
- expect(result[0].beforeDetails).toBe("");
709
- expect(result[0].afterDetails).toBe("");
710
- });
711
- it("IGNORE with no matching existingTests entry: both Skipped", async () => {
712
- const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
713
- tmpDirs.push(tmpDir);
714
- const stateFile = await writeReconciledStateFile(tmpDir, [], [{ ...baseVerdict, action: DriftAction.Ignore }]);
715
- const result = await submitAndReadMaintenance(tmpDir, stateFile);
716
- expect(result[0].beforeStatus).toBe("Skipped");
717
- expect(result[0].afterStatus).toBe("Skipped");
718
- expect(result[0].beforeDetails).toBe("");
719
- expect(result[0].afterDetails).toBe("");
720
- });
721
- it("DELETE with no matching existingTests entry: beforeStatus=Unknown, afterStatus=Skipped", async () => {
722
- const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
723
- tmpDirs.push(tmpDir);
724
- const stateFile = await writeReconciledStateFile(tmpDir, [], [{ ...baseVerdict, action: DriftAction.Delete }]);
725
- const result = await submitAndReadMaintenance(tmpDir, stateFile);
726
- expect(result[0].beforeStatus).toBe("Unknown");
727
- expect(result[0].afterStatus).toBe("Skipped");
728
- });
729
- // ── With matching existingTests entry ─────────────────────────────────────
730
- it("UPDATE with before=Pass after=Pass: status from stateFile, details from the LLM-drafted testMaintenanceDetails", async () => {
731
- const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
732
- tmpDirs.push(tmpDir);
733
- const stateFile = await writeReconciledStateFile(tmpDir, [{
734
- testFile: baseVerdict.testFilePath,
735
- executionBefore: { status: TestExecutionStatus.Pass, errors: [], duration: 15090 },
736
- executionAfter: { status: TestExecutionStatus.Pass, errors: [], duration: 12880 },
737
- }], [{ ...baseVerdict, action: DriftAction.Update }]);
738
- const result = await submitAndReadMaintenance(tmpDir, stateFile, [
739
- { testFilePath: baseVerdict.testFilePath, beforeDetails: "4 passed in 15.09s", afterDetails: "5 passed in 12.88s" },
740
- ]);
741
- expect(result[0].beforeStatus).toBe("Pass");
742
- expect(result[0].beforeDetails).toBe("4 passed in 15.09s");
743
- expect(result[0].afterStatus).toBe("Pass");
744
- expect(result[0].afterDetails).toBe("5 passed in 12.88s");
745
- });
746
- it("UPDATE with before=Pass after=Fail: rejects when afterDetails is missing for a recorded execution", async () => {
747
- const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
748
- tmpDirs.push(tmpDir);
749
- const stateFile = await writeReconciledStateFile(tmpDir, [{
750
- testFile: baseVerdict.testFilePath,
751
- executionBefore: { status: TestExecutionStatus.Pass, errors: [], duration: 15090 },
752
- executionAfter: { status: TestExecutionStatus.Fail, errors: ["check_schema fails, test isolation bug"], duration: 10940 },
753
- }], [{ ...baseVerdict, action: DriftAction.Update }]);
754
- const outputFile = path.join(tmpDir, "report.json");
755
- const result = await handler({
756
- summaryOutputFile: outputFile,
757
- stateFile,
758
- businessCaseAnalysis: "Test",
759
- newTestsCreated: [],
760
- testMaintenanceDetails: [{ testFilePath: baseVerdict.testFilePath, beforeDetails: "4 passed in 15.09s", afterDetails: "" }], // afterDetails left blank
761
- testResults: [],
762
- issuesFound: [],
763
- });
764
- expect(result.isError).toBe(true);
765
- expect(result.content[0].text).toContain("orders_contract_test.py (afterDetails)");
766
- });
767
- it("UPDATE with before=Pass after=Fail: afterDetails carries the LLM's drafted root cause when supplied", async () => {
768
- const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
769
- tmpDirs.push(tmpDir);
770
- const stateFile = await writeReconciledStateFile(tmpDir, [{
771
- testFile: baseVerdict.testFilePath,
772
- executionBefore: { status: TestExecutionStatus.Pass, errors: [], duration: 15090 },
773
- executionAfter: { status: TestExecutionStatus.Fail, errors: ["check_schema fails, test isolation bug"], duration: 10940 },
774
- }], [{ ...baseVerdict, action: DriftAction.Update }]);
775
- const result = await submitAndReadMaintenance(tmpDir, stateFile, [
776
- { testFilePath: baseVerdict.testFilePath, beforeDetails: "4 passed in 15.09s", afterDetails: "FAILED test_orders_get — check_schema fails, test isolation bug" },
777
- ]);
778
- expect(result[0].beforeStatus).toBe("Pass");
779
- expect(result[0].afterStatus).toBe("Fail");
780
- expect(result[0].afterDetails).toBe("FAILED test_orders_get — check_schema fails, test isolation bug");
781
- });
782
- it("UPDATE with crashed execution: status is Error", async () => {
783
- const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
784
- tmpDirs.push(tmpDir);
785
- const stateFile = await writeReconciledStateFile(tmpDir, [{
786
- testFile: baseVerdict.testFilePath,
787
- executionBefore: { status: TestExecutionStatus.Error, errors: ["Connection refused"], duration: 500 },
788
- executionAfter: { status: TestExecutionStatus.Pass, errors: [], duration: 2000 },
789
- }], [{ ...baseVerdict, action: DriftAction.Update }]);
790
- const result = await submitAndReadMaintenance(tmpDir, stateFile, [
791
- { testFilePath: baseVerdict.testFilePath, beforeDetails: "Error — Connection refused", afterDetails: "5 passed in 2.00s" },
792
- ]);
793
- expect(result[0].beforeStatus).toBe("Error");
794
- expect(result[0].afterStatus).toBe("Pass");
795
- });
796
- it("UPDATE Fail→Pass: counted as maintenanceRecovered in analytics", async () => {
797
- const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
798
- tmpDirs.push(tmpDir);
799
- const stateFile = await writeReconciledStateFile(tmpDir, [{
800
- testFile: baseVerdict.testFilePath,
801
- executionBefore: { status: TestExecutionStatus.Fail, errors: ["FAILED test_foo - assert False"], duration: 5000 },
802
- executionAfter: { status: TestExecutionStatus.Pass, errors: [], duration: 5000 },
803
- }], [{ ...baseVerdict, action: DriftAction.Update }]);
804
- await submitAndReadMaintenance(tmpDir, stateFile, [
805
- { testFilePath: baseVerdict.testFilePath, beforeDetails: "FAILED test_foo - assert False", afterDetails: "5 passed in 5.00s" },
806
- ]);
807
- expect(pushMCPToolEventMock.mock.calls.at(-1)?.[2].maintenanceRecovered).toBe("1");
808
- });
809
- it("UPDATE with no before-execution recorded: beforeStatus=Unknown, afterStatus=Pass, no beforeDetails required", async () => {
810
- const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
811
- tmpDirs.push(tmpDir);
812
- const stateFile = await writeReconciledStateFile(tmpDir, [{
813
- testFile: baseVerdict.testFilePath,
814
- // executionBefore absent — agent skipped before-phase
815
- executionAfter: { status: TestExecutionStatus.Pass, errors: [], duration: 2000 },
816
- }], [{ ...baseVerdict, action: DriftAction.Update }]);
817
- // Only afterDetails is meaningful — there's no recorded before-execution to summarize,
818
- // so beforeDetails is the empty-string sentinel (the field is required, not omittable).
819
- const result = await submitAndReadMaintenance(tmpDir, stateFile, [
820
- { testFilePath: baseVerdict.testFilePath, beforeDetails: "", afterDetails: "5 passed in 2.00s" },
821
- ]);
822
- expect(result[0].beforeStatus).toBe("Unknown");
823
- expect(result[0].afterStatus).toBe("Pass");
824
- });
825
- it("DELETE with before-execution, no after (file deleted): beforeStatus=Pass, afterStatus=Skipped, no afterDetails required", async () => {
826
- const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
827
- tmpDirs.push(tmpDir);
828
- const stateFile = await writeReconciledStateFile(tmpDir, [{
829
- testFile: baseVerdict.testFilePath,
830
- executionBefore: { status: TestExecutionStatus.Pass, errors: [], duration: 2000 },
831
- // executionAfter absent — file was deleted, nothing to run
832
- }], [{ ...baseVerdict, action: DriftAction.Delete }]);
833
- // Only beforeDetails is meaningful — there's no recorded after-execution to summarize,
834
- // so afterDetails is the empty-string sentinel (the field is required, not omittable).
835
- const result = await submitAndReadMaintenance(tmpDir, stateFile, [
836
- { testFilePath: baseVerdict.testFilePath, beforeDetails: "4 passed in 2.00s", afterDetails: "" },
837
- ]);
838
- expect(result[0].beforeStatus).toBe("Pass");
839
- expect(result[0].afterStatus).toBe("Skipped");
840
- });
841
- it("REGENERATE with before=Fail after=Pass: requires both details, rejects when either is missing", async () => {
842
- const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
843
- tmpDirs.push(tmpDir);
844
- const stateFile = await writeReconciledStateFile(tmpDir, [{
845
- testFile: baseVerdict.testFilePath,
846
- executionBefore: { status: TestExecutionStatus.Fail, errors: ["schema drift"], duration: 4000 },
847
- executionAfter: { status: TestExecutionStatus.Pass, errors: [], duration: 3000 },
848
- }], [{ ...baseVerdict, action: DriftAction.Regenerate }]);
849
- const outputFile = path.join(tmpDir, "report.json");
850
- const rejected = await handler({
851
- summaryOutputFile: outputFile,
852
- stateFile,
853
- businessCaseAnalysis: "Test",
854
- newTestsCreated: [],
855
- testMaintenanceDetails: [{ testFilePath: baseVerdict.testFilePath, beforeDetails: "FAILED — schema drift", afterDetails: "" }], // afterDetails left blank
856
- testResults: [],
857
- issuesFound: [],
858
- });
859
- expect(rejected.isError).toBe(true);
860
- expect(rejected.content[0].text).toContain("orders_contract_test.py (afterDetails)");
861
- const result = await submitAndReadMaintenance(tmpDir, stateFile, [
862
- { testFilePath: baseVerdict.testFilePath, beforeDetails: "FAILED — schema drift", afterDetails: "regenerated test now passes" },
863
- ]);
864
- expect(result[0].beforeStatus).toBe("Fail");
865
- expect(result[0].afterStatus).toBe("Pass");
866
- });
867
- it("VERIFY with a recorded execution: requires details even though VERIFY is not a change action", async () => {
868
- const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
869
- tmpDirs.push(tmpDir);
870
- const stateFile = await writeReconciledStateFile(tmpDir, [{
871
- testFile: baseVerdict.testFilePath,
872
- executionBefore: { status: TestExecutionStatus.Pass, errors: [], duration: 1500 },
873
- }], [{ ...baseVerdict, action: DriftAction.Verify }]);
874
- const outputFile = path.join(tmpDir, "report.json");
875
- const rejected = await handler({
876
- summaryOutputFile: outputFile,
877
- stateFile,
878
- businessCaseAnalysis: "Test",
879
- newTestsCreated: [],
880
- testMaintenanceDetails: [],
881
- testResults: [],
882
- issuesFound: [],
883
- });
884
- expect(rejected.isError).toBe(true);
885
- expect(rejected.content[0].text).toContain("orders_contract_test.py (beforeDetails)");
886
- const result = await submitAndReadMaintenance(tmpDir, stateFile, [
887
- { testFilePath: baseVerdict.testFilePath, beforeDetails: "confirmed still passes, no changes needed", afterDetails: "" },
888
- ]);
889
- expect(result[0].beforeStatus).toBe("Pass");
890
- });
891
- it("DELETE with a spurious executionAfter recorded: afterDetails is still required despite DELETE's default afterStatus being Skipped", async () => {
892
- const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
893
- tmpDirs.push(tmpDir);
894
- const stateFile = await writeReconciledStateFile(tmpDir, [{
895
- testFile: baseVerdict.testFilePath,
896
- executionBefore: { status: TestExecutionStatus.Pass, errors: [], duration: 2000 },
897
- // executionAfter present even though the file should have been deleted — documents
898
- // that the required-details gate keys off recorded executions, not the action type.
899
- executionAfter: { status: TestExecutionStatus.Pass, errors: [], duration: 1800 },
900
- }], [{ ...baseVerdict, action: DriftAction.Delete }]);
901
- const outputFile = path.join(tmpDir, "report.json");
902
- const rejected = await handler({
903
- summaryOutputFile: outputFile,
904
- stateFile,
905
- businessCaseAnalysis: "Test",
906
- newTestsCreated: [],
907
- testMaintenanceDetails: [{ testFilePath: baseVerdict.testFilePath, beforeDetails: "4 passed in 2.00s", afterDetails: "" }], // afterDetails left blank
908
- testResults: [],
909
- issuesFound: [],
910
- });
911
- expect(rejected.isError).toBe(true);
912
- expect(rejected.content[0].text).toContain("orders_contract_test.py (afterDetails)");
913
- });
914
- });
915
- it("testResult accepts videoPath field (optional)", async () => {
916
- const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
917
- tmpDirs.push(tmpDir);
918
- const outputFile = path.join(tmpDir, "report.json");
919
- const stateFile = await writeStateFile(tmpDir, []);
920
- const result = await handler({
921
- summaryOutputFile: outputFile,
922
- stateFile,
923
- businessCaseAnalysis: "UI changes for login flow",
924
- newTestsCreated: [],
925
- testMaintenance: [],
926
- testResults: [
927
- {
928
- testType: TestType.UI,
929
- endpoint: "GET /login",
930
- status: "Pass",
931
- details: "5.2s, login_ui_test.spec.ts",
932
- videoPath: "/workspace/.skyramp/videos/login_ui_test-abc12345/video.webm",
933
- },
934
- ],
935
- issuesFound: [],
936
- });
937
- expect(result.isError).toBeUndefined();
938
- const written = JSON.parse(await fs.readFile(outputFile, "utf-8"));
939
- expect(written.testResults).toHaveLength(1);
940
- expect(written.testResults[0].videoPath).toBe("/workspace/.skyramp/videos/login_ui_test-abc12345/video.webm");
941
- });
942
- it("testResult accepts entry without videoPath (optional field)", async () => {
943
- const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
944
- tmpDirs.push(tmpDir);
945
- const outputFile = path.join(tmpDir, "report.json");
946
- const stateFile = await writeStateFile(tmpDir, []);
947
- const result = await handler({
948
- summaryOutputFile: outputFile,
949
- stateFile,
950
- businessCaseAnalysis: "API endpoint changes",
951
- newTestsCreated: [],
952
- testMaintenance: [],
953
- testResults: [
954
- {
955
- testType: TestType.CONTRACT,
956
- endpoint: "POST /api/v1/products",
957
- status: "Pass",
958
- details: "2.1s, products_contract_test.py",
959
- // videoPath intentionally omitted
960
- },
961
- ],
962
- issuesFound: [],
963
- });
964
- expect(result.isError).toBeUndefined();
965
- const written = JSON.parse(await fs.readFile(outputFile, "utf-8"));
966
- expect(written.testResults).toHaveLength(1);
967
- expect(written.testResults[0].videoPath).toBeUndefined();
968
- });
969
- describe("testMaintenance derived from state (SKYR-3906)", () => {
970
- it("derives testMaintenance from stateData.maintenanceVerdicts when testMaintenance is omitted", async () => {
971
- const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
972
- tmpDirs.push(tmpDir);
973
- const outputFile = path.join(tmpDir, "report.json");
974
- const stateFile = await writeReconciledStateFile(tmpDir, [{ testFile: "/repo/tests/orders_contract_test.py" }], [{ testFilePath: "/repo/tests/orders_contract_test.py", action: DriftAction.Update, rationale: "Field added", testType: TestType.CONTRACT, endpoint: "GET /api/v1/orders" }]);
975
- const result = await handler({
976
- summaryOutputFile: outputFile,
977
- stateFile,
978
- businessCaseAnalysis: "Adds a field to orders",
979
- newTestsCreated: [],
980
- // testMaintenance intentionally omitted — should be derived from state.
981
- testResults: [],
982
- issuesFound: [],
983
- });
984
- expect(result.isError).toBeUndefined();
985
- const written = JSON.parse(await fs.readFile(outputFile, "utf-8"));
986
- expect(written.testMaintenance).toEqual([
987
- expect.objectContaining({
988
- fileName: "orders_contract_test.py",
989
- testType: TestType.CONTRACT,
990
- endpoint: "GET /api/v1/orders",
991
- action: DriftAction.Update,
992
- description: "Field added",
993
- }),
994
- ]);
995
- });
996
- it("rejects when maintenanceVerdicts is undefined and existing tests were present — skyramp_actions was never called", async () => {
997
- const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
998
- tmpDirs.push(tmpDir);
999
- const outputFile = path.join(tmpDir, "report.json");
1000
- // Plain writeStateFile with no maintenanceVerdicts override — undefined, distinct
1001
- // from skyramp_actions having run and recorded an empty array.
1002
- const stateFile = path.join(tmpDir, "analyze-changes-state.json");
1003
- await fs.writeFile(stateFile, JSON.stringify({
1004
- existingTests: [{ testFile: "/repo/tests/orders_contract_test.py" }],
1005
- metadata: { sessionId: "test-session", stateType: "unified", repositoryPath: tmpDir, createdAt: "2026-01-01T00:00:00.000Z", updatedAt: "2026-01-01T00:00:00.000Z" },
1006
- }), "utf-8");
1007
- const result = await handler({
1008
- summaryOutputFile: outputFile,
1009
- stateFile,
1010
- businessCaseAnalysis: "Adds a field to orders",
1011
- newTestsCreated: [],
1012
- testResults: [],
1013
- issuesFound: [],
1014
- });
1015
- expect(result.isError).toBe(true);
1016
- expect(result.content[0].text).toContain("skyramp_actions was not called");
1017
- });
1018
- it("allows maintenanceVerdicts to be undefined when there were no existing tests to assess", async () => {
1019
- const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
1020
- tmpDirs.push(tmpDir);
1021
- const outputFile = path.join(tmpDir, "report.json");
1022
- // No existing tests at all — nothing skyramp_actions could have lost, so skipping
1023
- // it entirely (never writing maintenanceVerdicts) is a valid, not an error, state.
1024
- const stateFile = path.join(tmpDir, "analyze-changes-state.json");
1025
- await fs.writeFile(stateFile, JSON.stringify({
1026
- existingTests: [],
1027
- metadata: { sessionId: "test-session", stateType: "unified", repositoryPath: tmpDir, createdAt: "2026-01-01T00:00:00.000Z", updatedAt: "2026-01-01T00:00:00.000Z" },
1028
- }), "utf-8");
1029
- const result = await handler({
1030
- summaryOutputFile: outputFile,
1031
- stateFile,
1032
- businessCaseAnalysis: "Adds a new endpoint with no prior coverage",
1033
- newTestsCreated: [],
1034
- testResults: [],
1035
- issuesFound: [],
1036
- });
1037
- expect(result.isError).toBeUndefined();
1038
- const written = JSON.parse(await fs.readFile(outputFile, "utf-8"));
1039
- expect(written.testMaintenance).toBeUndefined();
1040
- });
1041
- it("rejects when a related repo has reconciled maintenanceVerdicts — testMaintenance has no repository field to carry them", async () => {
1042
- const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
1043
- tmpDirs.push(tmpDir);
1044
- const outputFile = path.join(tmpDir, "report.json");
1045
- const stateFile = path.join(tmpDir, "analyze-changes-state.json");
1046
- const now = "2026-01-01T00:00:00.000Z";
1047
- await fs.writeFile(stateFile, JSON.stringify({
1048
- existingTests: [],
1049
- maintenanceVerdicts: [],
1050
- metadata: { sessionId: "test-session", stateType: "unified", repositoryPath: tmpDir, createdAt: now, updatedAt: now },
1051
- relatedRepos: {
1052
- "acme/backend": {
1053
- repositoryPath: "/repo/backend",
1054
- data: {
1055
- existingTests: [],
1056
- maintenanceVerdicts: [
1057
- { testFilePath: "/repo/backend/tests/orders_test.py", action: DriftAction.Update, rationale: "Field added", testType: TestType.CONTRACT, endpoint: "GET /api/v1/orders" },
1058
- ],
1059
- },
1060
- },
1061
- },
1062
- }), "utf-8");
1063
- const result = await handler({
1064
- summaryOutputFile: outputFile,
1065
- stateFile,
1066
- businessCaseAnalysis: "Test",
1067
- newTestsCreated: [],
1068
- testResults: [],
1069
- issuesFound: [],
1070
- });
1071
- expect(result.isError).toBe(true);
1072
- expect(result.content[0].text).toContain("acme/backend");
1073
- });
1074
- it("rejects when a recorded execution has no matching testMaintenanceDetails entry at all", async () => {
1075
- const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
1076
- tmpDirs.push(tmpDir);
1077
- const outputFile = path.join(tmpDir, "report.json");
1078
- const stateFile = await writeReconciledStateFile(tmpDir, [{
1079
- testFile: "/repo/tests/orders_contract_test.py",
1080
- executionBefore: { status: TestExecutionStatus.Fail, errors: ["assert 403 got 200"], duration: 4200 },
1081
- executionAfter: { status: TestExecutionStatus.Pass, errors: [], duration: 5090 },
1082
- }], [{ testFilePath: "/repo/tests/orders_contract_test.py", action: DriftAction.Update, rationale: "Field added", testType: TestType.CONTRACT, endpoint: "GET /api/v1/orders" }]);
1083
- const result = await handler({
1084
- summaryOutputFile: outputFile,
1085
- stateFile,
1086
- businessCaseAnalysis: "Adds a field to orders",
1087
- newTestsCreated: [],
1088
- // testMaintenanceDetails omitted entirely, despite both executions being recorded.
1089
- testResults: [],
1090
- issuesFound: [],
1091
- });
1092
- expect(result.isError).toBe(true);
1093
- expect(result.content[0].text).toContain("orders_contract_test.py (beforeDetails)");
1094
- expect(result.content[0].text).toContain("orders_contract_test.py (afterDetails)");
1095
- });
1096
- it("rejects a whitespace-only testMaintenanceDetails summary as if it were missing", async () => {
1097
- const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
1098
- tmpDirs.push(tmpDir);
1099
- const outputFile = path.join(tmpDir, "report.json");
1100
- const stateFile = await writeReconciledStateFile(tmpDir, [{
1101
- testFile: "/repo/tests/orders_contract_test.py",
1102
- executionBefore: { status: TestExecutionStatus.Fail, errors: ["assert 403 got 200"], duration: 4200 },
1103
- executionAfter: { status: TestExecutionStatus.Pass, errors: [], duration: 5090 },
1104
- }], [{ testFilePath: "/repo/tests/orders_contract_test.py", action: DriftAction.Update, rationale: "Field added", testType: TestType.CONTRACT, endpoint: "GET /api/v1/orders" }]);
1105
- const result = await handler({
1106
- summaryOutputFile: outputFile,
1107
- stateFile,
1108
- businessCaseAnalysis: "Adds a field to orders",
1109
- newTestsCreated: [],
1110
- testMaintenanceDetails: [{
1111
- testFilePath: "/repo/tests/orders_contract_test.py",
1112
- beforeDetails: " ",
1113
- afterDetails: "5 passed in 5.09s",
1114
- }],
1115
- testResults: [],
1116
- issuesFound: [],
1117
- });
1118
- expect(result.isError).toBe(true);
1119
- expect(result.content[0].text).toContain("orders_contract_test.py (beforeDetails)");
1120
- });
1121
- it("prefers the LLM-drafted testMaintenanceDetails summary when supplied", async () => {
1122
- const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
1123
- tmpDirs.push(tmpDir);
1124
- const outputFile = path.join(tmpDir, "report.json");
1125
- const stateFile = await writeReconciledStateFile(tmpDir, [{
1126
- testFile: "/repo/tests/orders_contract_test.py",
1127
- executionBefore: { status: TestExecutionStatus.Fail, errors: ["assert 403 got 200"], duration: 4200 },
1128
- executionAfter: { status: TestExecutionStatus.Pass, errors: [], duration: 5090 },
1129
- }], [{ testFilePath: "/repo/tests/orders_contract_test.py", action: DriftAction.Update, rationale: "Field added", testType: TestType.CONTRACT, endpoint: "GET /api/v1/orders" }]);
1130
- const result = await handler({
1131
- summaryOutputFile: outputFile,
1132
- stateFile,
1133
- businessCaseAnalysis: "Adds a field to orders",
1134
- newTestsCreated: [],
1135
- testMaintenanceDetails: [{
1136
- testFilePath: "/repo/tests/orders_contract_test.py",
1137
- beforeDetails: "FAILED test_orders_get — auth middleware not enforced for the new field",
1138
- afterDetails: "5 passed in 5.09s",
1139
- }],
1140
- testResults: [],
1141
- issuesFound: [],
1142
- });
1143
- expect(result.isError).toBeUndefined();
1144
- const written = JSON.parse(await fs.readFile(outputFile, "utf-8"));
1145
- expect(written.testMaintenance[0].beforeDetails).toBe("FAILED test_orders_get — auth middleware not enforced for the new field");
1146
- expect(written.testMaintenance[0].afterDetails).toBe("5 passed in 5.09s");
1147
- });
1148
- it("rejects when only one of beforeDetails/afterDetails is supplied but both executions were recorded", async () => {
1149
- const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
1150
- tmpDirs.push(tmpDir);
1151
- const outputFile = path.join(tmpDir, "report.json");
1152
- const stateFile = await writeReconciledStateFile(tmpDir, [{
1153
- testFile: "/repo/tests/orders_contract_test.py",
1154
- executionBefore: { status: TestExecutionStatus.Fail, errors: ["assert 403 got 200"], duration: 4200 },
1155
- executionAfter: { status: TestExecutionStatus.Pass, errors: [], duration: 5090 },
1156
- }], [{ testFilePath: "/repo/tests/orders_contract_test.py", action: DriftAction.Update, rationale: "Field added", testType: TestType.CONTRACT, endpoint: "GET /api/v1/orders" }]);
1157
- const result = await handler({
1158
- summaryOutputFile: outputFile,
1159
- stateFile,
1160
- businessCaseAnalysis: "Adds a field to orders",
1161
- newTestsCreated: [],
1162
- testMaintenanceDetails: [{
1163
- testFilePath: "/repo/tests/orders_contract_test.py",
1164
- beforeDetails: "FAILED test_orders_get — auth middleware not enforced for the new field",
1165
- afterDetails: "", // left blank, but an after-execution was recorded — must be rejected.
1166
- }],
1167
- testResults: [],
1168
- issuesFound: [],
1169
- });
1170
- expect(result.isError).toBe(true);
1171
- expect(result.content[0].text).toContain("orders_contract_test.py (afterDetails)");
1172
- expect(result.content[0].text).not.toContain("beforeDetails");
1173
- });
1174
- });
1175
- describe("approvedPlan cross-check (SKYR-3879 Path B)", () => {
1176
- async function writeStateFileWithPlan(dir, approvedPlan) {
1177
- const stateFile = path.join(dir, "analyze-changes-state.json");
1178
- await fs.writeFile(stateFile, JSON.stringify({
1179
- existingTests: [],
1180
- maintenanceVerdicts: [],
1181
- ...(approvedPlan ? { approvedPlan } : {}),
1182
- metadata: sampleMetadata(dir),
1183
- }), "utf-8");
1184
- return stateFile;
1185
- }
1186
- const APPROVED_PLAN = {
1187
- planId: "plan-42",
1188
- createdAt: "2026-01-01T00:00:00.000Z",
1189
- generate: [
1190
- {
1191
- candidateId: "products-search-abcd1234",
1192
- scenarioName: "products_search",
1193
- testType: "contract",
1194
- category: "business_rule",
1195
- source: CandidateSource.AGENT,
1196
- matchKeys: ["name:products-search", "ep:contract:GET:/api/v1/products/search"],
1197
- },
1198
- ],
1199
- additional: [],
1200
- demotions: [],
1201
- };
1202
- function contractNewTest(overrides = {}) {
1203
- return {
1204
- testId: "contract-get-products-search",
1205
- testType: TestType.CONTRACT,
1206
- category: "business_rule",
1207
- endpoint: "GET /api/v1/products/search",
1208
- fileName: "products_search_contract_test.py",
1209
- description: "Searches products and asserts the response schema.",
1210
- reasoning: "Validates the contract for the new search endpoint.",
1211
- ...overrides,
1212
- };
1213
- }
1214
- it("passes when every gated newTestsCreated entry matches an approved GENERATE item", async () => {
1215
- const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
1216
- tmpDirs.push(tmpDir);
1217
- const outputFile = path.join(tmpDir, "report.json");
1218
- const stateFile = await writeStateFileWithPlan(tmpDir, APPROVED_PLAN);
1219
- const result = await handler({
1220
- summaryOutputFile: outputFile,
1221
- stateFile,
1222
- businessCaseAnalysis: "Adds product search.",
1223
- newTestsCreated: [contractNewTest()],
1224
- testResults: [],
1225
- issuesFound: [],
1226
- });
1227
- expect(result.isError).toBeUndefined();
1228
- });
1229
- it("rejects an unapproved gated entry, naming the offending entry and the approved list", async () => {
1230
- const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
1231
- tmpDirs.push(tmpDir);
1232
- const outputFile = path.join(tmpDir, "report.json");
1233
- const stateFile = await writeStateFileWithPlan(tmpDir, APPROVED_PLAN);
1234
- const result = await handler({
1235
- summaryOutputFile: outputFile,
1236
- stateFile,
1237
- businessCaseAnalysis: "Adds an unregistered contract test.",
1238
- newTestsCreated: [
1239
- contractNewTest({
1240
- testId: "contract-get-products-unregistered",
1241
- endpoint: "GET /api/v1/products/unregistered",
1242
- }),
1243
- ],
1244
- testResults: [],
1245
- issuesFound: [],
1246
- });
1247
- expect(result.isError).toBe(true);
1248
- expect(result.content[0].text).toContain("plan-42");
1249
- expect(result.content[0].text).toContain("contract-get-products-unregistered");
1250
- expect(result.content[0].text).toContain("products_search");
1251
- });
1252
- it("accepts a gated entry matching an approved ADDITIONAL item — backfill parity with planGuard", async () => {
1253
- const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
1254
- tmpDirs.push(tmpDir);
1255
- const outputFile = path.join(tmpDir, "report.json");
1256
- const stateFile = await writeStateFileWithPlan(tmpDir, {
1257
- ...APPROVED_PLAN,
1258
- additional: [
1259
- {
1260
- candidateId: "products-backfill-efgh5678",
1261
- scenarioName: "products_backfill",
1262
- testType: "contract",
1263
- category: "business_rule",
1264
- source: CandidateSource.AGENT,
1265
- matchKeys: ["name:products-backfill", "ep:contract:GET:/api/v1/products/backfill"],
1266
- },
1267
- ],
1268
- });
1269
- const result = await handler({
1270
- summaryOutputFile: outputFile,
1271
- stateFile,
1272
- businessCaseAnalysis: "GENERATE item failed; backfilled from ADDITIONAL as the plan instructs.",
1273
- newTestsCreated: [
1274
- contractNewTest({
1275
- testId: "contract-get-products-backfill",
1276
- endpoint: "GET /api/v1/products/backfill",
1277
- }),
1278
- ],
1279
- testResults: [],
1280
- issuesFound: [],
1281
- });
1282
- expect(result.isError).toBeUndefined();
1283
- });
1284
- it("rejects an ADDITIONAL-matching entry when the GENERATE list is empty — zero-budget plans have nothing to backfill", async () => {
1285
- const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
1286
- tmpDirs.push(tmpDir);
1287
- const outputFile = path.join(tmpDir, "report.json");
1288
- const stateFile = await writeStateFileWithPlan(tmpDir, {
1289
- ...APPROVED_PLAN,
1290
- generate: [],
1291
- additional: APPROVED_PLAN.generate,
1292
- });
1293
- const result = await handler({
1294
- summaryOutputFile: outputFile,
1295
- stateFile,
1296
- businessCaseAnalysis: "Generated a test on a zero-budget plan.",
1297
- newTestsCreated: [contractNewTest()],
1298
- testResults: [],
1299
- issuesFound: [],
1300
- });
1301
- expect(result.isError).toBe(true);
1302
- expect(result.content[0].text).toContain("plan-42");
1303
- });
1304
- it("does not check newTestsCreated when approvedPlan is absent (back-compat)", async () => {
1305
- const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
1306
- tmpDirs.push(tmpDir);
1307
- const outputFile = path.join(tmpDir, "report.json");
1308
- const stateFile = await writeStateFileWithPlan(tmpDir, undefined);
1309
- const result = await handler({
1310
- summaryOutputFile: outputFile,
1311
- stateFile,
1312
- businessCaseAnalysis: "No plan was ever registered for this run.",
1313
- newTestsCreated: [contractNewTest({ endpoint: "GET /api/v1/products/anything" })],
1314
- testResults: [],
1315
- issuesFound: [],
1316
- });
1317
- expect(result.isError).toBeUndefined();
1318
- });
1319
- it("does not gate UI/E2E entries even when unmatched — those tools are outside planGuard's scope", async () => {
1320
- const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
1321
- tmpDirs.push(tmpDir);
1322
- const outputFile = path.join(tmpDir, "report.json");
1323
- const stateFile = await writeStateFileWithPlan(tmpDir, APPROVED_PLAN);
1324
- const result = await handler({
1325
- summaryOutputFile: outputFile,
1326
- stateFile,
1327
- businessCaseAnalysis: "Adds a UI test not present in the approved plan.",
1328
- newTestsCreated: [
1329
- {
1330
- testId: "ui-checkout-flow",
1331
- testType: TestType.UI,
1332
- category: "workflow",
1333
- endpoint: "N/A",
1334
- fileName: "checkout_ui_test.spec.ts",
1335
- description: "Walks through the checkout flow. [no-blueprint-data]",
1336
- reasoning: "Covers the new checkout UI. [no-blueprint-data]",
1337
- targetElements: null,
1338
- },
1339
- ],
1340
- testResults: [],
1341
- issuesFound: [],
1342
- });
1343
- expect(result.isError).toBeUndefined();
1344
- });
1345
- });
1346
- });
1347
- // SKYR-3883: end-to-end coverage of the working-tree verification gate. The unit
1348
- // behavior lives in reportVerification.test.ts; these exercise the wiring through
1349
- // the real tool handler (feature-flag gating, git enumeration, error surfacing).
1350
- describe("submit_report working-tree verification (SKYR-3883)", () => {
1351
- let handler;
1352
- let tmpDirs = [];
1353
- const prevFlag = process.env.SKYRAMP_FEATURE_TESTBOT;
1354
- beforeAll(() => {
1355
- handler = captureToolHandler();
1356
- });
1357
- beforeEach(() => {
1358
- process.env.SKYRAMP_FEATURE_TESTBOT = "1";
1359
- });
1360
- afterEach(async () => {
1361
- if (prevFlag === undefined)
1362
- delete process.env.SKYRAMP_FEATURE_TESTBOT;
1363
- else
1364
- process.env.SKYRAMP_FEATURE_TESTBOT = prevFlag;
1365
- for (const dir of tmpDirs)
1366
- await fs.rm(dir, { recursive: true, force: true });
1367
- tmpDirs = [];
1368
- });
1369
- async function initGitRepo() {
1370
- const dir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-wt-"));
1371
- tmpDirs.push(dir);
1372
- execFileSync("git", ["init", "-q"], { cwd: dir });
1373
- execFileSync("git", ["config", "user.email", "t@t.dev"], { cwd: dir });
1374
- execFileSync("git", ["config", "user.name", "t"], { cwd: dir });
1375
- return dir;
1376
- }
1377
- it("rejects a report whose newTestsCreated file was never written", async () => {
1378
- const dir = await initGitRepo();
1379
- const outputFile = path.join(dir, "report.json");
1380
- const stateFile = await writeStateFile(dir, []);
1381
- // search_smoke_test.py is claimed but never created → unbacked.
1382
- const result = await handler(sampleReportParams(outputFile, stateFile));
1383
- expect(result.isError).toBe(true);
1384
- expect(result.content[0].text).toContain("not backed");
1385
- expect(result.content[0].text).toContain("search_smoke_test.py");
1386
- });
1387
- it("accepts the report once the claimed file exists in the working tree", async () => {
1388
- const dir = await initGitRepo();
1389
- const outputFile = path.join(dir, "report.json");
1390
- const stateFile = await writeStateFile(dir, []);
1391
- await fs.writeFile(path.join(dir, "search_smoke_test.py"), "def test_search(): pass\n");
1392
- const result = await handler(sampleReportParams(outputFile, stateFile));
1393
- expect(result.isError).toBeUndefined();
1394
- });
1395
- it("rejects an UPDATE maintenance verdict with no working-tree change, exempting DELETE", async () => {
1396
- const dir = await initGitRepo();
1397
- const outputFile = path.join(dir, "report.json");
1398
- const stateFile = await writeReconciledStateFile(dir, [], [
1399
- { testFilePath: path.join(dir, "dcim/tests/test_filtersets.py"), action: DriftAction.Update, rationale: "physical_address filter", testType: TestType.INTEGRATION, endpoint: "GET /api/dcim/sites/" },
1400
- { testFilePath: path.join(dir, "dcim/tests/gone.py"), action: DriftAction.Delete, rationale: "obsolete", testType: TestType.INTEGRATION, endpoint: "GET /api/dcim/sites/" },
1401
- ]);
1402
- // Keep the newTestsCreated claim backed so the failure isolates to the verdict.
1403
- await fs.writeFile(path.join(dir, "search_smoke_test.py"), "x\n");
1404
- const result = await handler(sampleReportParams(outputFile, stateFile));
1405
- expect(result.isError).toBe(true);
1406
- expect(result.content[0].text).toContain("test_filtersets.py");
1407
- // DELETE is applied at delivery, never a working-tree claim — must not be flagged.
1408
- expect(result.content[0].text).not.toContain("gone.py");
1409
- });
1410
- it("skips verification entirely when the testbot feature flag is off", async () => {
1411
- delete process.env.SKYRAMP_FEATURE_TESTBOT;
1412
- const dir = await initGitRepo();
1413
- const outputFile = path.join(dir, "report.json");
1414
- const stateFile = await writeStateFile(dir, []);
1415
- // Claimed file absent, but the gate is off → report goes through.
1416
- const result = await handler(sampleReportParams(outputFile, stateFile));
1417
- expect(result.isError).toBeUndefined();
1418
- });
1419
- it("degrades to a pass when repositoryPath is not a git repo (verification can't run)", async () => {
1420
- const dir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-nogit-"));
1421
- tmpDirs.push(dir);
1422
- const outputFile = path.join(dir, "report.json");
1423
- const stateFile = await writeStateFile(dir, []); // repositoryPath=dir, but no git init
1424
- const result = await handler(sampleReportParams(outputFile, stateFile));
1425
- // git status throws → caught → warning → report not falsely rejected.
1426
- expect(result.isError).toBeUndefined();
1427
- });
1428
- });