@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,983 +0,0 @@
1
- // @ts-nocheck - Jest ESM type inference issues
2
- import { jest } from "@jest/globals";
3
- // Mock heavy dependencies
4
- jest.mock("simple-git", () => ({
5
- simpleGit: jest.fn(() => ({
6
- checkIsRepo: jest.fn().mockResolvedValue(false),
7
- })),
8
- }));
9
- jest.unstable_mockModule("../utils/logger.js", () => ({
10
- logger: {
11
- info: jest.fn(),
12
- debug: jest.fn(),
13
- error: jest.fn(),
14
- warning: jest.fn(),
15
- },
16
- }));
17
- // fast-glob must return real file paths from the temp directory.
18
- // We use the real implementation by not mocking it.
19
- import * as fs from "fs";
20
- import * as path from "path";
21
- import * as os from "os";
22
- import { TestSource } from "../types/TestAnalysis.js";
23
- const { TestDiscoveryService } = await import("./TestDiscoveryService.js");
24
- describe("TestDiscoveryService", () => {
25
- let service;
26
- let tmpDir;
27
- beforeEach(() => {
28
- service = new TestDiscoveryService();
29
- tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "test-discovery-"));
30
- });
31
- afterEach(() => {
32
- fs.rmSync(tmpDir, { recursive: true, force: true });
33
- });
34
- function writeFile(relPath, content) {
35
- const fullPath = path.join(tmpDir, relPath);
36
- fs.mkdirSync(path.dirname(fullPath), { recursive: true });
37
- fs.writeFileSync(fullPath, content, "utf-8");
38
- return fullPath;
39
- }
40
- // ── classifyTestFiles ──
41
- describe("classifyTestFiles", () => {
42
- it("classifies files with Skyramp marker as skyramp", async () => {
43
- writeFile("tests/test_orders_smoke.py", '# Generated by Skyramp\nimport skyramp\nskyramp generate smoke rest');
44
- const result = await service.discoverTests(tmpDir);
45
- const skyrampTests = result.tests.filter((t) => t.source === TestSource.Skyramp);
46
- expect(skyrampTests.length).toBe(1);
47
- expect(skyrampTests[0].testFile).toContain("test_orders_smoke.py");
48
- });
49
- it("classifies test files without Skyramp marker as external", async () => {
50
- writeFile("tests/test_orders.py", 'import pytest\nimport requests\nresponse = requests.get("/api/orders")');
51
- const result = await service.discoverTests(tmpDir);
52
- const externalTests = result.tests.filter((t) => t.source === TestSource.External);
53
- expect(externalTests.length).toBe(1);
54
- expect(externalTests[0].testFile).toContain("test_orders.py");
55
- });
56
- it("skips non-test files that lack the Skyramp marker", async () => {
57
- writeFile("src/utils.py", 'def helper():\n return True');
58
- const result = await service.discoverTests(tmpDir);
59
- expect(result.tests.length).toBe(0);
60
- });
61
- it("handles both Skyramp and external tests in the same repo", async () => {
62
- writeFile("tests/test_orders_smoke.py", '# Generated by Skyramp\nskyramp generate smoke rest');
63
- writeFile("tests/test_api.py", 'import pytest\nrequests.get("/api/users")');
64
- const result = await service.discoverTests(tmpDir);
65
- expect(result.tests.length).toBe(2);
66
- expect(result.tests.filter((t) => t.source === TestSource.Skyramp).length).toBe(1);
67
- expect(result.tests.filter((t) => t.source === TestSource.External).length).toBe(1);
68
- });
69
- });
70
- // ── isExternalTestFile (via classification) ──
71
- describe("external test file detection", () => {
72
- const patterns = [
73
- ["test_orders.py", true],
74
- ["orders_test.py", true],
75
- ["orders.test.ts", true],
76
- ["orders.spec.js", true],
77
- ["OrdersTest.java", true],
78
- ["orders_test.go", true],
79
- ["orders_spec.rb", true],
80
- ["OrdersTest.kt", true],
81
- ["OrdersTests.cs", true],
82
- ["orders_test.rs", true],
83
- ["OrdersTest.php", true],
84
- ["test_orders.rb", true],
85
- ["utils.py", false],
86
- ["main.ts", false],
87
- ["main.go", false],
88
- ];
89
- it.each(patterns)("file %s → isExternal: %s", async (filename, expected) => {
90
- writeFile(filename, 'import pytest\ndef test_something():\n pass');
91
- const result = await service.discoverTests(tmpDir);
92
- const externalTests = result.tests.filter((t) => t.source === TestSource.External);
93
- if (expected) {
94
- expect(externalTests.length).toBe(1);
95
- }
96
- else {
97
- expect(externalTests.length).toBe(0);
98
- }
99
- });
100
- it("detects files in test directories", async () => {
101
- writeFile("tests/helpers.py", 'import requests\nrequests.get("/api/health")');
102
- const result = await service.discoverTests(tmpDir);
103
- const externalTests = result.tests.filter((t) => t.source === TestSource.External);
104
- expect(externalTests.length).toBe(1);
105
- });
106
- it("detects files in __tests__ directory", async () => {
107
- writeFile("src/__tests__/api.ts", 'import axios from "axios";\naxios.get("/api/items");');
108
- const result = await service.discoverTests(tmpDir);
109
- const externalTests = result.tests.filter((t) => t.source === TestSource.External);
110
- expect(externalTests.length).toBe(1);
111
- });
112
- });
113
- // ── extractExternalEndpoints ──
114
- describe("extractExternalEndpoints", () => {
115
- it("extracts endpoints from Python requests library", async () => {
116
- writeFile("test_api.py", 'import requests\nrequests.get("/api/orders")\nrequests.post("/api/orders")');
117
- const result = await service.discoverTests(tmpDir);
118
- const ext = result.tests.find((t) => t.source === TestSource.External);
119
- expect(ext).toBeDefined();
120
- expect(ext.apiEndpoint).toContain("GET /api/orders");
121
- expect(ext.apiEndpoint).toContain("POST /api/orders");
122
- });
123
- it("extracts endpoints from Python test client", async () => {
124
- writeFile("test_api.py", 'def test_orders(client):\n client.get("/api/orders")\n client.post("/api/orders")');
125
- const result = await service.discoverTests(tmpDir);
126
- const ext = result.tests.find((t) => t.source === TestSource.External);
127
- expect(ext).toBeDefined();
128
- expect(ext.apiEndpoint).toContain("GET /api/orders");
129
- });
130
- it("extracts endpoints from axios", async () => {
131
- writeFile("api.test.ts", 'import axios from "axios";\naxios.get("/api/items");\naxios.post("/api/items");');
132
- const result = await service.discoverTests(tmpDir);
133
- const ext = result.tests.find((t) => t.source === TestSource.External);
134
- expect(ext).toBeDefined();
135
- expect(ext.apiEndpoint).toContain("GET /api/items");
136
- expect(ext.apiEndpoint).toContain("POST /api/items");
137
- });
138
- it("extracts endpoints from supertest", async () => {
139
- writeFile("api.test.ts", 'import supertest from "supertest";\nconst req = supertest(app);\nreq.get("/api/users");\nreq.post("/api/users");');
140
- const result = await service.discoverTests(tmpDir);
141
- const ext = result.tests.find((t) => t.source === TestSource.External);
142
- expect(ext).toBeDefined();
143
- expect(ext.apiEndpoint).toContain("GET /api/users");
144
- expect(ext.apiEndpoint).toContain("POST /api/users");
145
- });
146
- it("extracts endpoints from fetch with explicit method", async () => {
147
- writeFile("api.test.ts", 'fetch("/api/orders", { method: "POST" });\nfetch("/api/orders");');
148
- const result = await service.discoverTests(tmpDir);
149
- const ext = result.tests.find((t) => t.source === TestSource.External);
150
- expect(ext).toBeDefined();
151
- expect(ext.apiEndpoint).toContain("POST /api/orders");
152
- expect(ext.apiEndpoint).toContain("GET /api/orders");
153
- });
154
- it("strips base URL from endpoints", async () => {
155
- writeFile("test_api.py", 'import requests\nrequests.get("http://localhost:8000/api/orders")');
156
- const result = await service.discoverTests(tmpDir);
157
- const ext = result.tests.find((t) => t.source === TestSource.External);
158
- expect(ext).toBeDefined();
159
- expect(ext.apiEndpoint).toContain("GET /api/orders");
160
- expect(ext.apiEndpoint).not.toContain("localhost");
161
- });
162
- it("extracts endpoints from Java RestAssured", async () => {
163
- writeFile("OrdersTest.java", 'import io.restassured.*;\ngiven().when().get("/api/orders").then();');
164
- const result = await service.discoverTests(tmpDir);
165
- const ext = result.tests.find((t) => t.source === TestSource.External);
166
- expect(ext).toBeDefined();
167
- expect(ext.apiEndpoint).toContain("GET /api/orders");
168
- });
169
- it("extracts endpoints from Go http.NewRequest", async () => {
170
- writeFile("orders_test.go", 'package main\nimport "testing"\nimport "net/http"\nfunc TestOrders(t *testing.T) {\n req, _ := http.NewRequest("GET", "/api/orders", nil)\n http.NewRequest("POST", "/api/orders", body)\n}');
171
- const result = await service.discoverTests(tmpDir);
172
- const ext = result.tests.find((t) => t.source === TestSource.External);
173
- expect(ext).toBeDefined();
174
- expect(ext.apiEndpoint).toContain("GET /api/orders");
175
- expect(ext.apiEndpoint).toContain("POST /api/orders");
176
- });
177
- it("extracts endpoints from C# HttpClient", async () => {
178
- writeFile("OrdersTests.cs", 'using Xunit;\npublic class OrdersTests {\n [Fact]\n public async Task GetOrders() {\n await client.GetAsync("/api/orders");\n await client.PostAsync("/api/orders", content);\n }\n}');
179
- const result = await service.discoverTests(tmpDir);
180
- const ext = result.tests.find((t) => t.source === TestSource.External);
181
- expect(ext).toBeDefined();
182
- expect(ext.apiEndpoint).toContain("GET /api/orders");
183
- expect(ext.apiEndpoint).toContain("POST /api/orders");
184
- });
185
- it("extracts endpoints from Ruby Net::HTTP", async () => {
186
- writeFile("orders_spec.rb", 'require "rspec"\nRSpec.describe "Orders" do\n it "lists" do\n Net::HTTP.get("/api/orders")\n Net::HTTP.post("/api/orders", body)\n end\nend');
187
- const result = await service.discoverTests(tmpDir);
188
- const ext = result.tests.find((t) => t.source === TestSource.External);
189
- expect(ext).toBeDefined();
190
- expect(ext.apiEndpoint).toContain("GET /api/orders");
191
- });
192
- });
193
- // ── detectExternalTestType ──
194
- describe("detectExternalTestType", () => {
195
- it("detects integration from filename", async () => {
196
- writeFile("test_orders_integration.py", "import pytest\ndef test(): pass");
197
- const result = await service.discoverTests(tmpDir);
198
- const ext = result.tests.find((t) => t.source === TestSource.External);
199
- expect(ext.testType).toBe("integration");
200
- });
201
- it("detects e2e from filename", async () => {
202
- writeFile("test_e2e_flow.py", "import pytest\ndef test(): pass");
203
- const result = await service.discoverTests(tmpDir);
204
- const ext = result.tests.find((t) => t.source === TestSource.External);
205
- expect(ext.testType).toBe("e2e");
206
- });
207
- it("detects contract from filename", async () => {
208
- writeFile("test_contract.py", "import pytest\ndef test(): pass");
209
- const result = await service.discoverTests(tmpDir);
210
- const ext = result.tests.find((t) => t.source === TestSource.External);
211
- expect(ext.testType).toBe("contract");
212
- });
213
- it("falls back to integration for files with HTTP calls", async () => {
214
- writeFile("test_api.py", 'import requests\nrequests.get("/api/orders")');
215
- const result = await service.discoverTests(tmpDir);
216
- const ext = result.tests.find((t) => t.source === TestSource.External);
217
- expect(ext.testType).toBe("integration");
218
- });
219
- it("returns unknown when no heuristic matches", async () => {
220
- writeFile("test_utils.py", "def test_helper():\n assert True");
221
- const result = await service.discoverTests(tmpDir);
222
- const ext = result.tests.find((t) => t.source === TestSource.External);
223
- expect(ext.testType).toBe("unknown");
224
- });
225
- });
226
- // ── detectExternalFramework ──
227
- describe("detectExternalFramework", () => {
228
- it("detects pytest", async () => {
229
- writeFile("test_api.py", "import pytest\ndef test(): pass");
230
- const result = await service.discoverTests(tmpDir);
231
- const ext = result.tests.find((t) => t.source === TestSource.External);
232
- expect(ext.framework).toBe("pytest");
233
- });
234
- it("detects jest", async () => {
235
- writeFile("api.test.ts", 'import { jest } from "jest";\ndescribe("api", () => {});');
236
- const result = await service.discoverTests(tmpDir);
237
- const ext = result.tests.find((t) => t.source === TestSource.External);
238
- expect(ext.framework).toBe("jest");
239
- });
240
- it("detects junit", async () => {
241
- writeFile("OrdersTest.java", "import org.junit.Test;\n@Test\npublic void test() {}");
242
- const result = await service.discoverTests(tmpDir);
243
- const ext = result.tests.find((t) => t.source === TestSource.External);
244
- expect(ext.framework).toBe("junit");
245
- });
246
- it("detects supertest", async () => {
247
- writeFile("api.test.ts", 'import supertest from "supertest";\nconst req = supertest(app);');
248
- const result = await service.discoverTests(tmpDir);
249
- const ext = result.tests.find((t) => t.source === TestSource.External);
250
- expect(ext.framework).toBe("supertest");
251
- });
252
- it("detects go-testing", async () => {
253
- writeFile("orders_test.go", 'package main\nimport "testing"\nfunc TestOrders(t *testing.T) {}');
254
- const result = await service.discoverTests(tmpDir);
255
- const ext = result.tests.find((t) => t.source === TestSource.External);
256
- expect(ext.framework).toBe("go-testing");
257
- });
258
- it("detects rspec", async () => {
259
- writeFile("orders_spec.rb", 'require "rspec"\nRSpec.describe "Orders" do\n it "works" do; end\nend');
260
- const result = await service.discoverTests(tmpDir);
261
- const ext = result.tests.find((t) => t.source === TestSource.External);
262
- expect(ext.framework).toBe("rspec");
263
- });
264
- it("detects xunit", async () => {
265
- writeFile("OrdersTests.cs", 'using Xunit;\npublic class OrdersTests {\n [Fact]\n public void Test() {}\n}');
266
- const result = await service.discoverTests(tmpDir);
267
- const ext = result.tests.find((t) => t.source === TestSource.External);
268
- expect(ext.framework).toBe("xunit");
269
- });
270
- it("detects phpunit", async () => {
271
- writeFile("OrdersTest.php", '<?php\nuse PHPUnit\\Framework\\TestCase;\nclass OrdersTest extends TestCase {\n public function testList() {}\n}');
272
- const result = await service.discoverTests(tmpDir);
273
- const ext = result.tests.find((t) => t.source === TestSource.External);
274
- expect(ext.framework).toBe("phpunit");
275
- });
276
- it("detects rust-test", async () => {
277
- writeFile("orders_test.rs", '#[cfg(test)]\nmod tests {\n #[test]\n fn test_orders() {}\n}');
278
- const result = await service.discoverTests(tmpDir);
279
- const ext = result.tests.find((t) => t.source === TestSource.External);
280
- expect(ext.framework).toBe("rust-test");
281
- });
282
- });
283
- // ── source field ──
284
- describe("source field tagging", () => {
285
- it("tags Skyramp tests with source: skyramp", async () => {
286
- writeFile("test_smoke.py", '# Generated by Skyramp\nskyramp generate smoke rest');
287
- const result = await service.discoverTests(tmpDir);
288
- expect(result.tests[0].source).toBe(TestSource.Skyramp);
289
- });
290
- it("tags external tests with source: external", async () => {
291
- writeFile("test_api.py", "import pytest\ndef test(): pass");
292
- const result = await service.discoverTests(tmpDir);
293
- expect(result.tests[0].source).toBe(TestSource.External);
294
- });
295
- });
296
- // ── relevance-based partitioning ──
297
- describe("relevance-based external test partitioning", () => {
298
- it("returns all external test files without a hard cap in PR mode", async () => {
299
- for (let i = 0; i < 60; i++) {
300
- writeFile(`test_file_${i}.py`, `def test_${i}(): pass`);
301
- }
302
- const result = await service.discoverTests(tmpDir, { changedResources: ["file"] });
303
- const externalTests = result.tests.filter((t) => t.source === TestSource.External);
304
- expect(externalTests.length).toBe(60);
305
- });
306
- it("extracts endpoints only for relevant files when changedResources provided", async () => {
307
- writeFile("test_orders_api.py", 'import requests\nrequests.get("/api/orders")');
308
- writeFile("test_products_api.py", 'import requests\nrequests.get("/api/products")');
309
- const result = await service.discoverTests(tmpDir, { changedResources: ["orders"] });
310
- const externalTests = result.tests.filter((t) => t.source === TestSource.External);
311
- // Only the relevant file (orders) is included — low-relevance files are excluded entirely.
312
- expect(externalTests.length).toBe(1);
313
- const ordersTest = externalTests.find(t => t.testFile.includes("orders"));
314
- expect(ordersTest?.apiEndpoint).toContain("GET /api/orders");
315
- // Low-relevance products test is not in result.tests at all.
316
- expect(externalTests.find(t => t.testFile.includes("products"))).toBeUndefined();
317
- });
318
- it("returns relevantExternalTestPaths including files where extraction may have failed", async () => {
319
- writeFile("test_orders_api.py", 'import requests\nrequests.get("/api/orders")');
320
- writeFile("test_orders_helpers.py", 'def create_order(): pass'); // no extractable endpoints
321
- writeFile("test_products_api.py", 'import requests\nrequests.get("/api/products")');
322
- const result = await service.discoverTests(tmpDir, { changedResources: ["orders"] });
323
- // Both orders-related files should be in relevantExternalTestPaths
324
- expect(result.relevantExternalTestPaths.some(p => p.includes("test_orders_api"))).toBe(true);
325
- expect(result.relevantExternalTestPaths.some(p => p.includes("test_orders_helpers"))).toBe(true);
326
- // Products file should NOT be in relevantExternalTestPaths
327
- expect(result.relevantExternalTestPaths.some(p => p.includes("test_products_api"))).toBe(false);
328
- });
329
- it("includes a file matched only by changedSymbols content grep (SKYR-3924 schema-only)", async () => {
330
- // Path/URL do not token-match the resource; the file is relevant only because it
331
- // references the changed symbol by name — the schema/DTO-only diff case.
332
- writeFile("test_schema.py", "from app.schemas import DeploymentCreate\ndef test_ctor():\n DeploymentCreate(name='x')");
333
- writeFile("test_unrelated.py", "def test_noop(): pass");
334
- const result = await service.discoverTests(tmpDir, {
335
- changedResources: ["workpool"],
336
- changedSymbols: ["DeploymentCreate"],
337
- });
338
- expect(result.relevantExternalTestPaths.some((p) => p.includes("test_schema"))).toBe(true);
339
- expect(result.relevantExternalTestPaths.some((p) => p.includes("test_unrelated"))).toBe(false);
340
- });
341
- it("keeps direct endpoint-call matches uncapped when preciseResources is set", async () => {
342
- // MAX_CONTENT_PROMOTED = 5; create 7 files that each POST the changed endpoint but whose
343
- // names do not token-match the resource, so they route through endpoint-call matching.
344
- for (let i = 0; i < 7; i++) {
345
- writeFile(`test_case_${i}.py`, 'import requests\nrequests.post("/api/deployments/")');
346
- }
347
- const precise = await service.discoverTests(tmpDir, {
348
- changedResources: ["deployments"],
349
- preciseResources: true,
350
- });
351
- expect(precise.relevantExternalTestPaths.length).toBe(7);
352
- // Without preciseResources the strong endpoint-call matches are capped together with
353
- // loose mentions at MAX_CONTENT_PROMOTED.
354
- const capped = await service.discoverTests(tmpDir, { changedResources: ["deployments"] });
355
- expect(capped.relevantExternalTestPaths.length).toBe(5);
356
- });
357
- it("does full extraction for all files when no changedResources provided", async () => {
358
- writeFile("test_orders_api.py", 'import requests\nrequests.get("/api/orders")');
359
- const result = await service.discoverTests(tmpDir);
360
- const ext = result.tests.find((t) => t.source === TestSource.External);
361
- expect(ext?.apiEndpoint).toContain("GET /api/orders");
362
- });
363
- it("caps full-repo mode at MAX_EXTERNAL_FULL_REPO for full extraction", async () => {
364
- // Create 12 external test files to test the boundary (MAX_EXTERNAL_FULL_REPO=100 is too
365
- // large to create in a unit test, so we verify the partitioning logic is wired correctly)
366
- for (let i = 0; i < 12; i++) {
367
- writeFile(`test_api_${i}.py`, `import requests\nrequests.get("/api/items/${i}")`);
368
- }
369
- const result = await service.discoverTests(tmpDir);
370
- const externalTests = result.tests.filter(t => t.source === TestSource.External);
371
- // All 12 files should be discovered
372
- expect(externalTests.length).toBe(12);
373
- // In full-repo mode (no changedResources) all files under cap go to relevantExternal
374
- expect(result.relevantExternalTestPaths.length).toBe(12);
375
- // All relevant files get full extraction (apiEndpoint populated)
376
- const withEndpoints = externalTests.filter(t => t.apiEndpoint !== "");
377
- expect(withEndpoints.length).toBe(12);
378
- });
379
- it("returns zero external tests when changedResources is empty array (PR mode, no endpoints)", async () => {
380
- // Simulate PR mode where a parsed diff produced no detected endpoints:
381
- // newEndpoints=[], modifiedEndpoints=[], and removedEndpoints=[] → changedResources = []
382
- writeFile("test_orders_api.py", 'import requests\nrequests.get("/api/orders")');
383
- writeFile("test_products_api.py", 'import requests\nrequests.get("/api/products")');
384
- const result = await service.discoverTests(tmpDir, { changedResources: [] });
385
- const externalTests = result.tests.filter(t => t.source === TestSource.External);
386
- // Empty changedResources = PR mode with no detected endpoints → zero external tests
387
- expect(externalTests.length).toBe(0);
388
- expect(result.relevantExternalTestPaths.length).toBe(0);
389
- });
390
- it("still returns external tests in full-repo mode (changedResources undefined)", async () => {
391
- // Full-repo mode: changedResources not provided → should use capped full-repo behavior
392
- writeFile("test_orders_api.py", 'import requests\nrequests.get("/api/orders")');
393
- writeFile("test_products_api.py", 'import requests\nrequests.get("/api/products")');
394
- const result = await service.discoverTests(tmpDir); // no options → undefined
395
- const externalTests = result.tests.filter(t => t.source === TestSource.External);
396
- expect(externalTests.length).toBe(2);
397
- expect(result.relevantExternalTestPaths.length).toBe(2);
398
- });
399
- it("Skyramp tests are unaffected by empty changedResources", async () => {
400
- writeFile("tests/test_orders_smoke.py", '# Generated by Skyramp\nskyramp generate smoke rest');
401
- writeFile("test_external.py", 'import pytest\ndef test(): pass');
402
- const result = await service.discoverTests(tmpDir, { changedResources: [] });
403
- const skyrampTests = result.tests.filter(t => t.source === TestSource.Skyramp);
404
- const externalTests = result.tests.filter(t => t.source === TestSource.External);
405
- // Skyramp tests always discovered regardless of changedResources
406
- expect(skyrampTests.length).toBe(1);
407
- // External tests suppressed in PR-mode-no-endpoints
408
- expect(externalTests.length).toBe(0);
409
- });
410
- it("returns no external tests with ['unknown'] sentinel (unresolvable resources)", async () => {
411
- // When diff endpoints exist but all paths resolve to "unknown" (e.g. decorator-relative
412
- // paths like "/{order_id}"), changedResources = ["unknown"]. No external tests score > 0
413
- // since "unknown" doesn't match any filename tokens — low-relevance files are excluded.
414
- writeFile("test_orders_api.py", 'import requests\nrequests.get("/api/orders")');
415
- writeFile("test_products_api.py", 'import requests\nrequests.get("/api/products")');
416
- const result = await service.discoverTests(tmpDir, { changedResources: ["unknown"] });
417
- const externalTests = result.tests.filter(t => t.source === TestSource.External);
418
- expect(externalTests.length).toBe(0);
419
- expect(result.relevantExternalTestPaths.length).toBe(0);
420
- });
421
- it("low-relevance files are excluded from result.tests in PR mode", async () => {
422
- writeFile("test_orders_api.py", 'import requests\nrequests.get("/api/orders")');
423
- writeFile("test_products_api.py", 'import requests\nrequests.get("/api/products")');
424
- const result = await service.discoverTests(tmpDir, { changedResources: ["orders"] });
425
- // products scored 0 — excluded entirely, not even as name-only entry
426
- const productsTest = result.tests.find(t => t.testFile.includes("test_products_api"));
427
- expect(productsTest).toBeUndefined();
428
- });
429
- });
430
- });
431
- // ---------------------------------------------------------------------------
432
- // Framework detection ordering — private method behaviour tested via discoverTests
433
- // ---------------------------------------------------------------------------
434
- describe("TestDiscoveryService — detectExternalFramework ordering", () => {
435
- let service;
436
- let tmpDir;
437
- beforeEach(() => {
438
- service = new TestDiscoveryService();
439
- tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "tds-fw-"));
440
- });
441
- afterEach(() => { fs.rmSync(tmpDir, { recursive: true, force: true }); });
442
- function writeFile(rel, content) {
443
- const fp = path.join(tmpDir, rel);
444
- fs.mkdirSync(path.dirname(fp), { recursive: true });
445
- fs.writeFileSync(fp, content, "utf-8");
446
- return fp;
447
- }
448
- it("pytest-playwright file → playwright-ui (not pytest)", async () => {
449
- writeFile("tests/test_login.py", "import pytest\nfrom playwright.sync_api import Page\ndef test_login(page: Page):\n page.goto('http://localhost')");
450
- const result = await service.discoverTests(tmpDir, { changedResources: ["login"] });
451
- const t = result.tests.find(f => f.testFile.includes("test_login.py"));
452
- expect(t?.framework).toBe("playwright-ui");
453
- });
454
- it("Playwright API-only test (request, no page) → playwright (not playwright-ui)", async () => {
455
- writeFile("tests/test_api.py", "from playwright.sync_api import APIRequestContext\ndef test_orders(request: APIRequestContext):\n request.get('/api/orders')");
456
- const result = await service.discoverTests(tmpDir, { changedResources: ["orders"] });
457
- const t = result.tests.find(f => f.testFile.includes("test_api.py"));
458
- expect(t?.framework).toBe("playwright");
459
- });
460
- it("pure pytest file → pytest", async () => {
461
- writeFile("tests/test_calc.py", "import pytest\ndef test_add():\n assert 1 + 1 == 2");
462
- const result = await service.discoverTests(tmpDir, { changedResources: ["calc"] });
463
- const t = result.tests.find(f => f.testFile.includes("test_calc.py"));
464
- expect(t?.framework).toBe("pytest");
465
- });
466
- it("RTL file with it()/expect() → rtl (not jest)", async () => {
467
- writeFile("src/Cart.test.tsx", "import { render } from '@testing-library/react';\nit('renders', () => { expect(true).toBe(true); });");
468
- const result = await service.discoverTests(tmpDir, { changedResources: ["cart"], changedFrontendFiles: ["src/Cart.tsx"] });
469
- const t = result.tests.find(f => f.testFile.includes("Cart.test.tsx"));
470
- expect(t?.framework).toBe("rtl");
471
- });
472
- it("vue-test-utils file → vue-test-utils (not jest)", async () => {
473
- writeFile("src/Counter.test.ts", "import { mount } from '@testing-library/vue';\nit('works', () => { expect(true).toBe(true); });");
474
- const result = await service.discoverTests(tmpDir, { changedResources: ["counter"], changedFrontendFiles: ["src/Counter.tsx"] });
475
- const t = result.tests.find(f => f.testFile.includes("Counter.test.ts"));
476
- expect(t?.framework).toBe("vue-test-utils");
477
- });
478
- it("JS Playwright file using page.goto() → playwright-ui", async () => {
479
- writeFile("e2e/login.spec.ts", "import { test } from '@playwright/test';\ntest('login', async ({ page }) => { await page.goto('/'); });");
480
- const result = await service.discoverTests(tmpDir, { changedResources: ["login"] });
481
- const t = result.tests.find(f => f.testFile.includes("login.spec.ts"));
482
- expect(t?.framework).toBe("playwright-ui");
483
- });
484
- it("JS Playwright using only request fixture → playwright", async () => {
485
- writeFile("e2e/api.spec.ts", "import { test } from '@playwright/test';\ntest('api', async ({ request }) => { await request.get('/api'); });");
486
- const result = await service.discoverTests(tmpDir, { changedResources: ["api"] });
487
- const t = result.tests.find(f => f.testFile.includes("api.spec.ts"));
488
- expect(t?.framework).toBe("playwright");
489
- });
490
- });
491
- // ---------------------------------------------------------------------------
492
- // detectExternalTestType path rules
493
- // ---------------------------------------------------------------------------
494
- describe("TestDiscoveryService — detectExternalTestType path rules", () => {
495
- let service;
496
- let tmpDir;
497
- beforeEach(() => {
498
- service = new TestDiscoveryService();
499
- tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "tds-type-"));
500
- });
501
- afterEach(() => { fs.rmSync(tmpDir, { recursive: true, force: true }); });
502
- function writeFile(rel, content) {
503
- const fp = path.join(tmpDir, rel);
504
- fs.mkdirSync(path.dirname(fp), { recursive: true });
505
- fs.writeFileSync(fp, content, "utf-8");
506
- return fp;
507
- }
508
- it("file in playwright/ dir → testType=e2e", async () => {
509
- writeFile("apps/web/playwright/login.spec.ts", "import { test } from '@playwright/test';\ntest('login', async ({ page }) => { await page.goto('/'); });");
510
- const result = await service.discoverTests(tmpDir, { changedResources: ["login"] });
511
- const t = result.tests.find(f => f.testFile.includes("login.spec.ts"));
512
- expect(t?.testType).toBe("e2e");
513
- });
514
- it("file with e2e in filename inside e2e/ dir → testType=e2e", async () => {
515
- // .e2e.ts in an e2e/ dir is recognized both by dir pattern and filename
516
- writeFile("apps/web/e2e/auth.e2e.ts", "import { test } from '@playwright/test';\ntest('auth', async ({ page }) => { await page.goto('/'); });");
517
- const result = await service.discoverTests(tmpDir, { changedResources: ["auth"] });
518
- const t = result.tests.find(f => f.testFile.includes("auth.e2e.ts"));
519
- expect(t?.testType).toBe("e2e");
520
- });
521
- it("file in api dir (no playwright in path) → NOT e2e", async () => {
522
- writeFile("apps/api/v2/tests/login.spec.ts", "import { test } from '@jest/globals';\ntest('login', () => { expect(true).toBe(true); });");
523
- const result = await service.discoverTests(tmpDir, { changedResources: ["login"] });
524
- const t = result.tests.find(f => f.testFile.includes("login.spec.ts"));
525
- expect(t?.testType).not.toBe("e2e");
526
- });
527
- it("generic spec file (no e2e/playwright signal) → NOT e2e", async () => {
528
- writeFile("modules/bookings/login.spec.ts", "import { test } from '@jest/globals';\ntest('login', () => {});");
529
- const result = await service.discoverTests(tmpDir, { changedResources: ["login"] });
530
- const t = result.tests.find(f => f.testFile.includes("login.spec.ts"));
531
- expect(t?.testType).not.toBe("e2e");
532
- });
533
- });
534
- // ---------------------------------------------------------------------------
535
- // isFrontendFile edge cases
536
- // ---------------------------------------------------------------------------
537
- const { isFrontendFile } = await import("../prompts/test-recommendation/scopeAssessment.js");
538
- describe("isFrontendFile edge cases", () => {
539
- it("pages/api/orders.tsx → false (Next.js API route, /api/ blocks all)", () => {
540
- expect(isFrontendFile("pages/api/orders.tsx")).toBe(false);
541
- });
542
- it("routes/product-card.tsx → true (.tsx in routes/ is page component)", () => {
543
- expect(isFrontendFile("routes/product-card.tsx")).toBe(true);
544
- });
545
- it("routes/orders.ts → false (.ts in routes/ is route handler)", () => {
546
- expect(isFrontendFile("routes/orders.ts")).toBe(false);
547
- });
548
- it("src/components/Cart.tsx → true", () => {
549
- expect(isFrontendFile("src/components/Cart.tsx")).toBe(true);
550
- });
551
- it("src/utils/helpers.ts → false (ambiguous extension, not in frontend dir)", () => {
552
- expect(isFrontendFile("src/utils/helpers.ts")).toBe(false);
553
- });
554
- it("app/modules/booking/BookingList.tsx → true", () => {
555
- expect(isFrontendFile("app/modules/booking/BookingList.tsx")).toBe(true);
556
- });
557
- });
558
- // ---------------------------------------------------------------------------
559
- // Content-based relevance cap (MAX_CONTENT_PROMOTED = 5)
560
- // ---------------------------------------------------------------------------
561
- describe("TestDiscoveryService — content-based relevance cap", () => {
562
- let service;
563
- let tmpDir;
564
- beforeEach(() => {
565
- service = new TestDiscoveryService();
566
- tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "tds-cap-"));
567
- });
568
- afterEach(() => { fs.rmSync(tmpDir, { recursive: true, force: true }); });
569
- function writeFile(rel, content) {
570
- const fp = path.join(tmpDir, rel);
571
- fs.mkdirSync(path.dirname(fp), { recursive: true });
572
- fs.writeFileSync(fp, content, "utf-8");
573
- return fp;
574
- }
575
- it("promotes at most MAX_CONTENT_PROMOTED=5 files via content match", async () => {
576
- // 10 test files that all reference /orders in their content
577
- for (let i = 0; i < 10; i++) {
578
- writeFile(`tests/test_feature_${i}.py`, `import requests\nrequests.get("/api/checkout_${i}")`);
579
- // content-match: all contain "/orders"
580
- fs.writeFileSync(path.join(tmpDir, `tests/test_feature_${i}.py`), `import requests\n# tests ordering\nrequests.get("/api/orders/item_${i}")`);
581
- }
582
- // changedResources=["unknown"] so all score 0 by filename; content scan fires
583
- const result = await service.discoverTests(tmpDir, { changedResources: ["orders"] });
584
- // At most 5 content-matched files should be in relevantExternalTestPaths
585
- expect(result.relevantExternalTestPaths.length).toBeLessThanOrEqual(5);
586
- });
587
- });
588
- // ---------------------------------------------------------------------------
589
- // scoreRelevanceByContent regex safety (dot/plus escaped)
590
- // ---------------------------------------------------------------------------
591
- describe("TestDiscoveryService — scoreRelevanceByContent regex safety", () => {
592
- let service;
593
- let tmpDir;
594
- beforeEach(() => {
595
- service = new TestDiscoveryService();
596
- tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "tds-regex-"));
597
- });
598
- afterEach(() => { fs.rmSync(tmpDir, { recursive: true, force: true }); });
599
- function writeFile(rel, content) {
600
- const fp = path.join(tmpDir, rel);
601
- fs.mkdirSync(path.dirname(fp), { recursive: true });
602
- fs.writeFileSync(fp, content, "utf-8");
603
- return fp;
604
- }
605
- it("resource with dot (order.items) does not match orderXitems via unescaped dot", async () => {
606
- writeFile("tests/test_order.py", `import requests\nrequests.get("/api/orderXitems")`);
607
- // "order.items" in changedResources — dot must be escaped
608
- const result = await service.discoverTests(tmpDir, { changedResources: ["order.items"] });
609
- // should NOT match (orderXitems ≠ order.items)
610
- expect(result.relevantExternalTestPaths.some(p => p.includes("test_order.py"))).toBe(false);
611
- });
612
- it("resource with dot matches correctly when content has /order.items", async () => {
613
- writeFile("tests/test_order_items.py", `import requests\nrequests.get("/api/order.items/list")`);
614
- const result = await service.discoverTests(tmpDir, { changedResources: ["order.items"] });
615
- expect(result.relevantExternalTestPaths.some(p => p.includes("test_order_items.py"))).toBe(true);
616
- });
617
- it("resource with plus does not throw SyntaxError", async () => {
618
- writeFile("tests/test_v2.py", `import requests\nrequests.get("/api/v2/items")`);
619
- // Should not throw
620
- await expect(service.discoverTests(tmpDir, { changedResources: ["v2+api"] })).resolves.toBeDefined();
621
- });
622
- });
623
- // ---------------------------------------------------------------------------
624
- // UI promotion gating: no changed frontend files ⇒ no .e2e.ts promotion
625
- // ---------------------------------------------------------------------------
626
- describe("TestDiscoveryService — UI promotion gating (changedFrontendFiles)", () => {
627
- let service;
628
- let tmpDir;
629
- beforeEach(() => {
630
- service = new TestDiscoveryService();
631
- tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "tds-nofe-"));
632
- });
633
- afterEach(() => { fs.rmSync(tmpDir, { recursive: true, force: true }); });
634
- function writeFile(rel, content) {
635
- const fp = path.join(tmpDir, rel);
636
- fs.mkdirSync(path.dirname(fp), { recursive: true });
637
- fs.writeFileSync(fp, content, "utf-8");
638
- return fp;
639
- }
640
- it("does not promote .e2e.ts files when no changed frontend files are supplied", async () => {
641
- writeFile("apps/web/playwright/onboarding.e2e.ts", "import { test } from '@playwright/test';\ntest('onboarding', async ({ page }) => { await page.goto('/onboarding'); });");
642
- // changedResources=[] (no API endpoints) + no changedFrontendFiles (config-only PR) ⇒ Step 2 skipped
643
- const result = await service.discoverTests(tmpDir, { changedResources: [] });
644
- expect(result.relevantExternalTestPaths.length).toBe(0);
645
- expect(result.tests.length).toBe(0);
646
- });
647
- it("promotes .e2e.ts files relevant to a changed frontend file", async () => {
648
- writeFile("apps/web/playwright/onboarding.e2e.ts", "import { test } from '@playwright/test';\ntest('onboarding', async ({ page }) => { await page.goto('/onboarding'); });");
649
- const result = await service.discoverTests(tmpDir, { changedResources: [], changedFrontendFiles: ["apps/web/onboarding.tsx"] });
650
- // .e2e.ts should be promoted (UI_TEST_EXT includes .e2e.ts; "onboarding" token matches)
651
- expect(result.tests.some(t => t.testFile.includes("onboarding.e2e.ts"))).toBe(true);
652
- });
653
- // ── Step 3: API test promotion ─────────────────────────────────────────────
654
- // Uses a minimal mirror of the prefect-testb file structure to validate that
655
- // the content-scored promotion finds test_deployments.py (which imports from
656
- // the changed module) but not unrelated schedule/events test files.
657
- describe("Step 3 API promotion (hasApiChanges)", () => {
658
- // Realistic import lines taken from prefect-testb actual files.
659
- // test_deployments.py imports from schemas.actions — matches the import-context
660
- // pattern "schemas[./]actions" built when schemas/actions.py changes.
661
- const TEST_DEPLOYMENTS_CONTENT = `\
662
- import pytest
663
- from httpx import AsyncClient
664
- from prefect.server.schemas.actions import DeploymentCreate, DeploymentUpdate
665
- from prefect.server import models, schemas
666
-
667
- class TestCreateDeployment:
668
- async def test_create_deployment(self, client):
669
- response = await client.post("/api/deployments/", json={"name": "test"})
670
- assert response.status_code == 201
671
-
672
- class TestCreateFlowRunFromDeployment:
673
- async def test_create_flow_run(self, client, deployment):
674
- response = await client.post(f"/api/deployments/{deployment.id}/create_flow_run", json={})
675
- assert response.status_code == 201
676
- `;
677
- // test_deployment_schedules.py imports from schedules, NOT schemas.actions
678
- const TEST_DEPLOYMENT_SCHEDULES_CONTENT = `\
679
- import pytest
680
- from prefect.server import models, schemas
681
- from prefect.server.schemas.core import DeploymentSchedule
682
-
683
- class TestReadDeploymentSchedules:
684
- async def test_read_schedules(self, client, deployment):
685
- response = await client.get(f"/api/deployments/{deployment.id}/schedules")
686
- assert response.status_code == 200
687
- `;
688
- // events/actions file has no deployment schema imports at all
689
- const TEST_EVENTS_ACTIONS_CONTENT = `\
690
- import pytest
691
- from prefect.server.events import actions
692
- from prefect.server.events.clients import AssertingEventsClient
693
-
694
- class TestPausingWorkQueue:
695
- async def test_pausing(self, session):
696
- action = actions.PauseWorkQueue(work_queue_id="test")
697
- await action.act(event=None)
698
- `;
699
- // test_schedules.py — no deployment imports
700
- const TEST_SCHEDULES_CONTENT = `\
701
- import pytest
702
- from prefect.server.schemas.schedules import CronSchedule, IntervalSchedule
703
-
704
- class TestCronSchedule:
705
- def test_cron_parse(self):
706
- s = CronSchedule(cron="0 * * * *")
707
- assert s.cron == "0 * * * *"
708
- `;
709
- it("content-matches test_deployments.py when schemas/actions.py changed", async () => {
710
- // Mirror the Prefect directory structure
711
- writeFile("tests/server/orchestration/api/test_deployments.py", TEST_DEPLOYMENTS_CONTENT);
712
- writeFile("tests/server/orchestration/api/test_deployment_schedules.py", TEST_DEPLOYMENT_SCHEDULES_CONTENT);
713
- writeFile("tests/events/server/actions/test_pausing_resuming_work_queue.py", TEST_EVENTS_ACTIONS_CONTENT);
714
- writeFile("tests/test_schedules.py", TEST_SCHEDULES_CONTENT);
715
- // changedResources = ["unknown"] because "actions" is filtered by GENERIC_SCHEMA_NAMES
716
- // in analyzeChangesTool. Step 3 is skipped for ["unknown"] sentinel.
717
- // So we use the import-context pattern directly via hasApiChanges + a non-unknown resource.
718
- // Simulate: schemas/actions.py changed, but it produced a non-generic parent resource.
719
- // For this test, use changedResources derived from the parent path "schemas":
720
- // "schemas" is in GENERIC_SCHEMA_NAMES so would be filtered → ["unknown"].
721
- // Step 3 should be skipped → test_deployments.py not promoted via Step 3.
722
- const resultUnknown = await service.discoverTests(tmpDir, {
723
- changedResources: ["unknown"],
724
- });
725
- // Step 3 skipped for ["unknown"] — no content-matched files
726
- const promotedUnknown = resultUnknown.relevantExternalTestPaths;
727
- expect(promotedUnknown.some(p => p.includes("test_deployments.py"))).toBe(false);
728
- });
729
- it("content-matches test_deployments.py when api/deployments.py changed (TM17 scenario)", async () => {
730
- writeFile("tests/server/orchestration/api/test_deployments.py", TEST_DEPLOYMENTS_CONTENT);
731
- writeFile("tests/server/orchestration/api/test_deployment_schedules.py", TEST_DEPLOYMENT_SCHEDULES_CONTENT);
732
- writeFile("tests/events/server/actions/test_pausing_resuming_work_queue.py", TEST_EVENTS_ACTIONS_CONTENT);
733
- writeFile("tests/test_schedules.py", TEST_SCHEDULES_CONTENT);
734
- // changedResources = ["deployments"] (from api/deployments.py route file)
735
- // Content pattern: /import\s+[^\n]*["']\/api\/deployments/i or /deployments/ prefix
736
- // test_deployments.py calls POST /api/deployments/ → content-matched in Step 3
737
- const result = await service.discoverTests(tmpDir, {
738
- changedResources: ["deployments"],
739
- });
740
- const promoted = result.relevantExternalTestPaths;
741
- // test_deployments.py: calls /api/deployments/ → content-matched → promoted unconditionally
742
- expect(promoted.some(p => p.includes("test_deployments.py"))).toBe(true);
743
- // test_deployment_schedules.py: contains /api/deployments/ in URL calls → also content-matched
744
- // (the word "deployments" appears in its URL strings even though it tests schedules)
745
- // This is expected — it IS in scope for a deployment endpoint change.
746
- // test_schedules.py: no /deployments/ URL → scores 0 → fallback only (may or may not be in promoted)
747
- // test_pausing_resuming: no /deployments/ URL → fallback only
748
- });
749
- it("API external tests no longer pre-loaded (agent greps instead)", async () => {
750
- // External API tests are discovered by the agent via the grep instruction in the
751
- // drift analysis prompt. discoverTests in PR mode (changedResources=[]) returns
752
- // only Skyramp tests; external tests are excluded entirely.
753
- writeFile("tests/server/orchestration/api/test_deployments.py", [
754
- "from prefect.server import utils",
755
- "",
756
- "async def test_max_active_runs(client, deployment):",
757
- " count = utils.calculate_max_runs(deployment.id, db)",
758
- " assert count >= 0",
759
- ].join("\n"));
760
- const result = await service.discoverTests(tmpDir, {
761
- changedResources: [], // PR mode, no pre-loading of external API tests
762
- });
763
- // No external tests pre-loaded — agent discovers via grep
764
- expect(result.relevantExternalTestPaths.some(p => p.includes("test_deployments.py"))).toBe(false);
765
- });
766
- it("Step 3 does not run when changedResources is empty (no API changes)", async () => {
767
- writeFile("tests/server/orchestration/api/test_deployments.py", TEST_DEPLOYMENTS_CONTENT);
768
- const result = await service.discoverTests(tmpDir, {
769
- changedResources: [],
770
- });
771
- // changedResources=[] means PR mode with no endpoints — external tests excluded entirely
772
- expect(result.relevantExternalTestPaths).toHaveLength(0);
773
- });
774
- it("Step 1 path-token scoring finds test_work_queues.py for changedResources=[work_queues]", async () => {
775
- // Pattern Q scenario: work_queues.py changed → changedResources=["work_queues"]
776
- // test_work_queues.py scores >0 by name (has "work_queues" token)
777
- writeFile("tests/server/orchestration/api/test_work_queues.py", 'import pytest\nfrom prefect.server.api import work_queues\n\nasync def test_read_work_queue_status(client, work_queue):\n r = await client.get(f"/api/work_queues/{work_queue.id}/status")\n assert r.status_code == 200\n');
778
- writeFile("tests/server/orchestration/api/test_flows.py", "import pytest\nfrom prefect.server import schemas\n\nasync def test_create_flow(client): pass\n");
779
- const result = await service.discoverTests(tmpDir, {
780
- changedResources: ["work_queues"],
781
- });
782
- const promoted = result.relevantExternalTestPaths;
783
- // test_work_queues.py → Step 1 name-scores it → in relevantExternal before Step 3
784
- expect(promoted.some(p => p.includes("test_work_queues.py"))).toBe(true);
785
- // test_flows.py → scores 0 → not promoted via Step 1
786
- // (may appear in Step 3 fallback but is not guaranteed)
787
- });
788
- });
789
- });
790
- // ---------------------------------------------------------------------------
791
- // SKYR-3941: UI-test promotion (Step 2) is scoped by relevance to the changed
792
- // frontend files — a frontend PR must not pull in the whole monorepo's UI suite.
793
- // ---------------------------------------------------------------------------
794
- describe("TestDiscoveryService — UI promotion scoping (SKYR-3941)", () => {
795
- let service;
796
- let tmpDir;
797
- // Minimal RTL content so the file classifies as an external UI test.
798
- const RTL = "import { render } from '@testing-library/react';\nit('renders', () => { expect(true).toBe(true); });";
799
- beforeEach(() => {
800
- service = new TestDiscoveryService();
801
- tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "tds-fe-"));
802
- });
803
- afterEach(() => { fs.rmSync(tmpDir, { recursive: true, force: true }); });
804
- function writeFile(rel, content) {
805
- const fp = path.join(tmpDir, rel);
806
- fs.mkdirSync(path.dirname(fp), { recursive: true });
807
- fs.writeFileSync(fp, content, "utf-8");
808
- return fp;
809
- }
810
- it("promotes UI tests relevant to the changed frontend files, not the whole suite", async () => {
811
- writeFile("src/routes/orders/OrderSearch.test.tsx", RTL);
812
- writeFile("packages/design-system/icons/__tests__/arrow-small-down.spec.tsx", RTL);
813
- // No API resources (Step 1 promotes nothing) — promotion is purely Step 2.
814
- const result = await service.discoverTests(tmpDir, {
815
- changedResources: [], changedFrontendFiles: ["src/routes/orders/OrderSearch.tsx"],
816
- });
817
- const promoted = result.relevantExternalTestPaths;
818
- expect(promoted.some(p => p.includes("OrderSearch.test.tsx"))).toBe(true);
819
- expect(promoted.some(p => p.includes("arrow-small-down.spec.tsx"))).toBe(false);
820
- });
821
- it("matches a camelCase changed component against a hyphenated test name", async () => {
822
- writeFile("src/order-search.spec.tsx", RTL);
823
- const result = await service.discoverTests(tmpDir, {
824
- changedResources: [], changedFrontendFiles: ["src/OrderSearch.tsx"],
825
- });
826
- expect(result.relevantExternalTestPaths.some(p => p.includes("order-search.spec.tsx"))).toBe(true);
827
- });
828
- it("uses the parent directory as the signal when the changed file's basename is generic (index.tsx)", async () => {
829
- writeFile("src/orders/OrdersTable.test.tsx", RTL);
830
- writeFile("src/billing/BillingPanel.test.tsx", RTL);
831
- const result = await service.discoverTests(tmpDir, {
832
- changedResources: [], changedFrontendFiles: ["src/orders/index.tsx"], // stem "index" generic → parent "orders" is the token
833
- });
834
- const promoted = result.relevantExternalTestPaths;
835
- expect(promoted.some(p => p.includes("OrdersTable.test.tsx"))).toBe(true);
836
- expect(promoted.some(p => p.includes("BillingPanel.test.tsx"))).toBe(false);
837
- });
838
- it("caps scoped promotion at the backstop even when many tests match", async () => {
839
- for (let i = 0; i < 55; i++)
840
- writeFile(`src/widgets/widget-${i}.test.tsx`, RTL);
841
- const result = await service.discoverTests(tmpDir, {
842
- changedResources: [], changedFrontendFiles: ["src/widgets/widget.tsx"], // token "widget" matches all 55
843
- });
844
- const promotedWidgets = result.relevantExternalTestPaths.filter(p => p.includes("widget-"));
845
- expect(promotedWidgets.length).toBe(50); // MAX_UI_PROMOTED backstop
846
- });
847
- it("promotes a UI test that imports a changed component even when its name shares no token (import-edge recall)", async () => {
848
- // "Checkout" shares no token with "PriceLabel" — only the import edge connects them.
849
- writeFile("src/features/Checkout.test.tsx", "import { render } from '@testing-library/react';\nimport { PriceLabel } from '../components/PriceLabel';\nit('shows price', () => { render(<PriceLabel />); });");
850
- writeFile("src/features/Billing.test.tsx", RTL); // no relevant import, no token overlap
851
- const result = await service.discoverTests(tmpDir, {
852
- changedResources: [], changedFrontendFiles: ["src/components/PriceLabel.tsx"],
853
- });
854
- const promoted = result.relevantExternalTestPaths;
855
- expect(promoted.some(p => p.includes("Checkout.test.tsx"))).toBe(true); // via import edge
856
- expect(promoted.some(p => p.includes("Billing.test.tsx"))).toBe(false);
857
- });
858
- it("does not let a generic-basename change (index.tsx) leak via import edge, but still matches on the parent dir", async () => {
859
- writeFile("src/orders/OrdersList.test.tsx", RTL); // matches parent token "orders" (pass 1)
860
- writeFile("src/widgets/Widget.test.tsx", "import { render } from '@testing-library/react';\nimport helpers from '../orders/index';\nit('x', () => {});"); // imports index — must NOT leak
861
- const result = await service.discoverTests(tmpDir, {
862
- changedResources: [], changedFrontendFiles: ["src/orders/index.tsx"],
863
- });
864
- const promoted = result.relevantExternalTestPaths;
865
- expect(promoted.some(p => p.includes("OrdersList.test.tsx"))).toBe(true); // parent-dir token
866
- expect(promoted.some(p => p.includes("Widget.test.tsx"))).toBe(false); // "/index'" signature skipped
867
- });
868
- it("falls back to capped promote-all when only generic entrypoints changed — never degenerates to zero (SKYR-3941)", async () => {
869
- // src/app/index.tsx: stem "index" and parent "app" are both generic → no tokens, and
870
- // "index" is skipped for import signatures → no signal at all. A root/entry change is
871
- // broadly impactful, so promotion must fall back rather than promote nothing.
872
- writeFile("src/routes/orders/OrderList.test.tsx", RTL);
873
- writeFile("src/products/ProductList.test.tsx", RTL);
874
- const result = await service.discoverTests(tmpDir, {
875
- changedResources: [], changedFrontendFiles: ["src/app/index.tsx"],
876
- });
877
- const promoted = result.relevantExternalTestPaths;
878
- // Neither test shares a token with "index"/"app" — without the fallback both would be
879
- // dropped (score 0). The no-signal fallback promotes them instead.
880
- expect(promoted.some(p => p.includes("OrderList.test.tsx"))).toBe(true);
881
- expect(promoted.some(p => p.includes("ProductList.test.tsx"))).toBe(true);
882
- });
883
- });
884
- // ---------------------------------------------------------------------------
885
- // Pass 3 — selector-edge promotion: page objects/specs couple to a changed
886
- // component by SELECTOR (data-testid / CSS class), not by filename or import.
887
- // ---------------------------------------------------------------------------
888
- describe("TestDiscoveryService — selector-edge promotion (changedSelectors)", () => {
889
- let service;
890
- let tmpDir;
891
- beforeEach(() => {
892
- service = new TestDiscoveryService();
893
- tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "tds-sel-"));
894
- });
895
- afterEach(() => { fs.rmSync(tmpDir, { recursive: true, force: true }); });
896
- function writeFile(rel, content) {
897
- const fp = path.join(tmpDir, rel);
898
- fs.mkdirSync(path.dirname(fp), { recursive: true });
899
- fs.writeFileSync(fp, content, "utf-8");
900
- return fp;
901
- }
902
- // A Playwright page object that selects a component by CSS class. It is NOT a
903
- // *.test/*.spec file (so passes 1-2 never consider it) and shares no name token
904
- // with the changed component, nor does it import it — the exact POM shape that
905
- // regressed after SKYR-3941. It still holds the OLD (renamed-away) selector.
906
- const POM_CONTENT = `\
907
- import { Locator, Page } from '@playwright/test';
908
-
909
- export class TableComponent {
910
- readonly columnFilter: Locator;
911
- constructor(private page: Page) {
912
- this.columnFilter = page.locator('.t--table-filter-columns-dropdown');
913
- }
914
- async openFilter() { await this.columnFilter.click(); }
915
- }
916
- `;
917
- it("promotes a page object that references a changed selector (not name/import matched)", async () => {
918
- writeFile("app/client/playwright/page-objects/components/table.component.ts", POM_CONTENT);
919
- const result = await service.discoverTests(tmpDir, {
920
- changedResources: [],
921
- // Changed component shares no token with "table.component" and the POM does not import it.
922
- changedFrontendFiles: ["app/client/src/widgets/TableWidgetV2/component/header/actions/filter/CascadeFields.tsx"],
923
- // Both the removed (old) and added (new) selector values, as the diff would carry them.
924
- changedSelectors: ["t--table-filter-columns-dropdown", "t--tbl-filter-column-select"],
925
- });
926
- expect(result.tests.some(t => t.testFile.includes("table.component.ts"))).toBe(true);
927
- expect(result.relevantExternalTestPaths.some(p => p.includes("table.component.ts"))).toBe(true);
928
- });
929
- it("does NOT promote the same page object when changedSelectors is absent (the regression baseline)", async () => {
930
- writeFile("app/client/playwright/page-objects/components/table.component.ts", POM_CONTENT);
931
- const result = await service.discoverTests(tmpDir, {
932
- changedResources: [],
933
- changedFrontendFiles: ["app/client/src/widgets/TableWidgetV2/component/header/actions/filter/CascadeFields.tsx"],
934
- // no changedSelectors → passes 1-2 only → name/import miss → POM dropped
935
- });
936
- expect(result.tests.some(t => t.testFile.includes("table.component.ts"))).toBe(false);
937
- });
938
- it("does not promote a page object that references no changed selector", async () => {
939
- writeFile("app/client/playwright/page-objects/components/unrelated.component.ts", "import { Page } from '@playwright/test';\nexport class Unrelated { constructor(p: Page) { p.locator('.t--some-other-widget-header'); } }");
940
- const result = await service.discoverTests(tmpDir, {
941
- changedResources: [],
942
- changedFrontendFiles: ["app/client/src/widgets/TableWidgetV2/component/header/actions/filter/CascadeFields.tsx"],
943
- changedSelectors: ["t--table-filter-columns-dropdown", "t--tbl-filter-column-select"],
944
- });
945
- expect(result.tests.some(t => t.testFile.includes("unrelated.component.ts"))).toBe(false);
946
- });
947
- it("promotes via a changed data-testid value (BEM/testid coupling)", async () => {
948
- writeFile("e2e/pages/confirmation-modal.page.ts", "import { Page } from '@playwright/test';\nexport class ConfirmationModal { constructor(p: Page) { this.confirm = p.getByTestId('confirmation-modal-confirm-button'); } }");
949
- const result = await service.discoverTests(tmpDir, {
950
- changedResources: [],
951
- changedFrontendFiles: ["src/modules/ui/layout/modal/components/ConfirmationModal.tsx"],
952
- changedSelectors: ["confirmation-modal-confirm-button", "confirmation-modal-primary-button"],
953
- });
954
- expect(result.tests.some(t => t.testFile.includes("confirmation-modal.page.ts"))).toBe(true);
955
- });
956
- it("promotes a page object via a dynamic BEM base prefix (shared-component modifier)", async () => {
957
- // The changed component composes its class dynamically (`ads-button--${kind}`), so only
958
- // the base prefix `ads-button--` is extractable; the POM's concrete modifier classes
959
- // (ads-button--primary/secondary) share that prefix and must still be promoted (SKYR-3943).
960
- writeFile("app/client/playwright/page-objects/ads-button.page.ts", "import { Page } from '@playwright/test';\nexport class AdsButton { constructor(p: Page) { this.primary = p.locator('.ads-button--primary'); this.secondary = p.locator('.ads-button--secondary'); } }");
961
- const result = await service.discoverTests(tmpDir, {
962
- changedResources: [],
963
- changedFrontendFiles: ["app/client/packages/design-system/ads/src/Button/Button.tsx"],
964
- changedSelectors: ["ads-button--"],
965
- });
966
- expect(result.tests.some(t => t.testFile.includes("ads-button.page.ts"))).toBe(true);
967
- });
968
- it("caps pass-3 selector promotion at MAX_UI_PROMOTED (no re-flood)", async () => {
969
- // 60 page objects all reference the same changed selector; pass 1/2 promote none of them
970
- // (names/imports don't match), so pass 3 must cap total promotion at MAX_UI_PROMOTED (50).
971
- for (let i = 0; i < 60; i++) {
972
- writeFile(`app/client/playwright/page-objects/pom-${i}.page.ts`, `import { Page } from '@playwright/test';\nexport class P${i} { constructor(p: Page) { p.locator('.t--shared-widget-hook'); } }`);
973
- }
974
- const result = await service.discoverTests(tmpDir, {
975
- changedResources: [],
976
- changedFrontendFiles: ["app/client/src/widgets/SharedWidget.tsx"],
977
- changedSelectors: ["t--shared-widget-hook"],
978
- });
979
- const promotedPoms = result.tests.filter(t => t.testFile.includes("/pom-")).length;
980
- expect(promotedPoms).toBeGreaterThan(0);
981
- expect(promotedPoms).toBeLessThanOrEqual(50);
982
- });
983
- });