@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.
- package/build/commands/commandLibrary.d.ts +1 -0
- package/build/commands/commandLibrary.js +19 -13
- package/build/commands/localDevTestChangesCommand.d.ts +15 -0
- package/build/commands/localDevTestChangesCommand.js +201 -0
- package/build/index.js +80 -6
- package/build/prompts/code-reuse.js +3 -0
- package/build/prompts/enhance-assertions/sharedAssertionRules.d.ts +1 -0
- package/build/prompts/enhance-assertions/sharedAssertionRules.js +17 -0
- package/build/prompts/initialize-workspace/initializeWorkspacePrompt.js +11 -9
- package/build/prompts/local-dev/local-dev-plan.d.ts +35 -0
- package/build/prompts/local-dev/local-dev-plan.js +429 -0
- package/build/prompts/local-dev/local-dev-prompts.d.ts +4 -0
- package/build/prompts/local-dev/local-dev-prompts.js +190 -0
- package/build/prompts/pom-aware-code-reuse.js +12 -0
- package/build/prompts/prompt-utils.d.ts +8 -0
- package/build/prompts/prompt-utils.js +33 -0
- package/build/prompts/sut-setup/modes/adaptWorkflowPrompt.js +37 -14
- package/build/prompts/sut-setup/shared.js +16 -12
- package/build/prompts/test-recommendation/analysisOutputPrompt.js +21 -29
- package/build/prompts/test-recommendation/scopeAssessment.d.ts +24 -7
- package/build/prompts/test-recommendation/scopeAssessment.js +111 -12
- package/build/prompts/test-recommendation/test-recommendation-prompt.js +41 -4
- package/build/prompts/testbot/testbot-prompts.d.ts +0 -5
- package/build/prompts/testbot/testbot-prompts.js +32 -52
- package/build/recommendation/planRanker.d.ts +15 -2
- package/build/recommendation/planRanker.js +76 -5
- package/build/resources/testbotResource.js +2 -1
- package/build/services/AnalyticsService.d.ts +1 -1
- package/build/services/TestExecutionService.d.ts +2 -1
- package/build/services/TestExecutionService.js +8 -3
- package/build/services/TestGenerationService.d.ts +2 -2
- package/build/services/TestGenerationService.js +39 -21
- package/build/services/containerEnv.js +3 -1
- package/build/tool-phases.js +6 -0
- package/build/tools/code-refactor/codeReuseTool.js +43 -4
- package/build/tools/code-refactor/enhanceAssertionsTool.js +68 -18
- package/build/tools/code-refactor/reuse-outcome.d.ts +109 -0
- package/build/tools/code-refactor/reuse-outcome.js +158 -0
- package/build/tools/code-refactor/reuse-state.d.ts +45 -0
- package/build/tools/code-refactor/reuse-state.js +140 -0
- package/build/tools/enrichTestWithMocksTool.d.ts +28 -0
- package/build/tools/enrichTestWithMocksTool.js +726 -0
- package/build/tools/executeSkyrampTestTool.d.ts +11 -0
- package/build/tools/executeSkyrampTestTool.js +62 -21
- package/build/tools/generate-tests/batchMockGenerationTool.d.ts +106 -0
- package/build/tools/generate-tests/batchMockGenerationTool.js +545 -0
- package/build/tools/generate-tests/generateBatchScenarioRestTool.js +25 -23
- package/build/tools/generate-tests/generateContractRestTool.js +2 -2
- package/build/tools/generate-tests/generateE2ERestTool.d.ts +1 -1
- package/build/tools/generate-tests/generateE2ERestTool.js +1 -1
- package/build/tools/generate-tests/generateLoadRestTool.d.ts +1 -1
- package/build/tools/generate-tests/generateLoadRestTool.js +1 -1
- package/build/tools/generate-tests/generateMockRestTool.d.ts +179 -6
- package/build/tools/generate-tests/generateMockRestTool.js +391 -22
- package/build/tools/generate-tests/loadTestSchema.d.ts +1 -1
- package/build/tools/generate-tests/planGuard.js +2 -22
- package/build/tools/generateEnrichedIntegrationTestTool.d.ts +25 -0
- package/build/tools/generateEnrichedIntegrationTestTool.js +235 -0
- package/build/tools/localDevWorkerComposeTool.d.ts +24 -0
- package/build/tools/localDevWorkerComposeTool.js +264 -0
- package/build/tools/one-click/oneClickTool.d.ts +13 -0
- package/build/tools/one-click/oneClickTool.js +195 -24
- package/build/tools/preflightMockCheckTool.d.ts +2 -0
- package/build/tools/preflightMockCheckTool.js +96 -0
- package/build/tools/queryProxyMocksTool.d.ts +70 -0
- package/build/tools/queryProxyMocksTool.js +522 -0
- package/build/tools/runExistingTestsTool.d.ts +31 -0
- package/build/tools/runExistingTestsTool.js +214 -13
- package/build/tools/submitReportTool.js +38 -3
- package/build/tools/test-management/analyzeChangesTool.d.ts +2 -1
- package/build/tools/test-management/analyzeChangesTool.js +63 -40
- package/build/tools/test-management/registerTestPlanTool.js +55 -7
- package/build/tools/trace/startTraceCollectionTool.js +3 -3
- package/build/types/FrontendIntegration.d.ts +3 -0
- package/build/types/FrontendIntegration.js +3 -0
- package/build/types/OneClickCommands.d.ts +1 -1
- package/build/types/Recommendation.d.ts +20 -0
- package/build/types/Recommendation.js +32 -6
- package/build/types/RepositoryAnalysis.d.ts +131 -14
- package/build/types/RepositoryAnalysis.js +16 -2
- package/build/types/ReuseOutcome.d.ts +63 -0
- package/build/types/ReuseOutcome.js +33 -0
- package/build/types/TestExecution.d.ts +1 -0
- package/build/types/TestTypes.d.ts +25 -7
- package/build/types/TestTypes.js +22 -6
- package/build/types/TestbotReport.d.ts +7 -0
- package/build/types/index.d.ts +2 -0
- package/build/types/index.js +1 -0
- package/build/utils/AnalysisStateManager.d.ts +26 -0
- package/build/utils/AnalysisStateManager.js +31 -1
- package/build/utils/analyze-openapi.js +18 -1
- package/build/utils/branchDiff.d.ts +17 -1
- package/build/utils/branchDiff.js +99 -14
- package/build/utils/featureFlags.d.ts +31 -0
- package/build/utils/featureFlags.js +37 -0
- package/build/utils/frontendIntegration.js +8 -1
- package/build/utils/grpcMockValidation.d.ts +1 -0
- package/build/utils/grpcMockValidation.js +49 -0
- package/build/utils/httpMethodValidation.d.ts +4 -0
- package/build/utils/httpMethodValidation.js +15 -0
- package/build/utils/logger.js +1 -1
- package/build/utils/mockCompatibility.d.ts +49 -0
- package/build/utils/mockCompatibility.js +82 -0
- package/build/utils/pom-scope/pom-files.js +7 -0
- package/build/utils/pom-verify/bindings.js +11 -1
- package/build/utils/pom-verify/re-exports.d.ts +10 -0
- package/build/utils/pom-verify/re-exports.js +61 -0
- package/build/utils/pom-verify/resolve.d.ts +4 -1
- package/build/utils/pom-verify/resolve.js +7 -4
- package/build/utils/pom-verify/verify.d.ts +5 -0
- package/build/utils/pom-verify/verify.js +25 -3
- package/build/utils/progress.js +10 -5
- package/build/utils/proxy-terminal.js +3 -3
- package/build/utils/routeParsers.d.ts +3 -9
- package/build/utils/routeParsers.js +79 -4
- package/build/utils/utils.js +2 -2
- package/build/utils/versions.d.ts +4 -3
- package/build/utils/versions.js +3 -1
- package/build/utils/workspaceAuth.d.ts +46 -0
- package/build/utils/workspaceAuth.js +156 -1
- package/build/workspace/testSuites.d.ts +2 -1
- package/build/workspace/testSuites.js +1 -1
- package/build/workspace/workspace.d.ts +108 -32
- package/build/workspace/workspace.js +32 -4
- package/package.json +5 -2
- package/build/adapters/jestAdapter.test.d.ts +0 -1
- package/build/adapters/jestAdapter.test.js +0 -93
- package/build/adapters/mochaAdapter.test.d.ts +0 -1
- package/build/adapters/mochaAdapter.test.js +0 -63
- package/build/adapters/playwrightAdapter.test.d.ts +0 -1
- package/build/adapters/playwrightAdapter.test.js +0 -169
- package/build/adapters/pytestAdapter.test.d.ts +0 -1
- package/build/adapters/pytestAdapter.test.js +0 -90
- package/build/prompts/code-reuse.test.d.ts +0 -1
- package/build/prompts/code-reuse.test.js +0 -62
- package/build/prompts/pom-aware-code-reuse.test.d.ts +0 -1
- package/build/prompts/pom-aware-code-reuse.test.js +0 -11
- package/build/prompts/test-maintenance/drift-analysis-prompt.test.d.ts +0 -1
- package/build/prompts/test-maintenance/drift-analysis-prompt.test.js +0 -51
- package/build/prompts/test-recommendation/analysisOutputPrompt.test.d.ts +0 -1
- package/build/prompts/test-recommendation/analysisOutputPrompt.test.js +0 -264
- package/build/prompts/test-recommendation/mergeEnrichedScenarios.test.d.ts +0 -1
- package/build/prompts/test-recommendation/mergeEnrichedScenarios.test.js +0 -126
- package/build/prompts/test-recommendation/promptPlan.test.d.ts +0 -1
- package/build/prompts/test-recommendation/promptPlan.test.js +0 -336
- package/build/prompts/test-recommendation/scopeAssessment.test.d.ts +0 -1
- package/build/prompts/test-recommendation/scopeAssessment.test.js +0 -371
- package/build/prompts/test-recommendation/test-recommendation-prompt.test.d.ts +0 -1
- package/build/prompts/test-recommendation/test-recommendation-prompt.test.js +0 -1925
- package/build/prompts/testbot/testbot-prompts.test.d.ts +0 -1
- package/build/prompts/testbot/testbot-prompts.test.js +0 -451
- package/build/recommendation/budgeters/diversityBalancedBudgeter.test.d.ts +0 -1
- package/build/recommendation/budgeters/diversityBalancedBudgeter.test.js +0 -138
- package/build/recommendation/budgeters/fixedNBudgeter.test.d.ts +0 -1
- package/build/recommendation/budgeters/fixedNBudgeter.test.js +0 -66
- package/build/recommendation/discriminators.test.d.ts +0 -1
- package/build/recommendation/discriminators.test.js +0 -324
- package/build/recommendation/diversity.test.d.ts +0 -1
- package/build/recommendation/diversity.test.js +0 -77
- package/build/recommendation/planRanker.test.d.ts +0 -1
- package/build/recommendation/planRanker.test.js +0 -110
- package/build/resources/testbotResource.test.d.ts +0 -1
- package/build/resources/testbotResource.test.js +0 -44
- package/build/services/AnalyticsService.test.d.ts +0 -1
- package/build/services/AnalyticsService.test.js +0 -86
- package/build/services/ScenarioGenerationService.integration.test.d.ts +0 -1
- package/build/services/ScenarioGenerationService.integration.test.js +0 -162
- package/build/services/ScenarioGenerationService.test.d.ts +0 -1
- package/build/services/ScenarioGenerationService.test.js +0 -414
- package/build/services/TestDiscoveryService.test.d.ts +0 -1
- package/build/services/TestDiscoveryService.test.js +0 -983
- package/build/services/TestExecutionService.test.d.ts +0 -1
- package/build/services/TestExecutionService.test.js +0 -1032
- package/build/services/TestGenerationService.test.d.ts +0 -1
- package/build/services/TestGenerationService.test.js +0 -578
- package/build/tool-phase-coverage.test.d.ts +0 -1
- package/build/tool-phase-coverage.test.js +0 -49
- package/build/tools/code-refactor/codeReuseTool.test.d.ts +0 -1
- package/build/tools/code-refactor/codeReuseTool.test.js +0 -572
- package/build/tools/generate-tests/generateBatchScenarioRestTool.test.d.ts +0 -1
- package/build/tools/generate-tests/generateBatchScenarioRestTool.test.js +0 -433
- package/build/tools/generate-tests/generateContractRestTool.test.d.ts +0 -1
- package/build/tools/generate-tests/generateContractRestTool.test.js +0 -142
- package/build/tools/generate-tests/generateIntegrationRestTool.test.d.ts +0 -1
- package/build/tools/generate-tests/generateIntegrationRestTool.test.js +0 -159
- package/build/tools/generate-tests/generateLoadRestTool.test.d.ts +0 -1
- package/build/tools/generate-tests/generateLoadRestTool.test.js +0 -169
- package/build/tools/generate-tests/generateUIRestTool.test.d.ts +0 -1
- package/build/tools/generate-tests/generateUIRestTool.test.js +0 -32
- package/build/tools/generate-tests/planGuard.test.d.ts +0 -1
- package/build/tools/generate-tests/planGuard.test.js +0 -185
- package/build/tools/generate-tests/scenarioLint.test.d.ts +0 -1
- package/build/tools/generate-tests/scenarioLint.test.js +0 -100
- package/build/tools/runExistingTestsTool.test.d.ts +0 -1
- package/build/tools/runExistingTestsTool.test.js +0 -379
- package/build/tools/submitReportTool.test.d.ts +0 -1
- package/build/tools/submitReportTool.test.js +0 -1428
- package/build/tools/test-management/actionsTool.test.d.ts +0 -1
- package/build/tools/test-management/actionsTool.test.js +0 -436
- package/build/tools/test-management/analyzeChangesTool.test.d.ts +0 -1
- package/build/tools/test-management/analyzeChangesTool.test.js +0 -522
- package/build/tools/test-management/analyzeTestHealthTool.test.d.ts +0 -1
- package/build/tools/test-management/analyzeTestHealthTool.test.js +0 -385
- package/build/tools/test-management/registerTestPlanTool.test.d.ts +0 -1
- package/build/tools/test-management/registerTestPlanTool.test.js +0 -296
- package/build/tools/trace/resolveSaveStoragePath.test.d.ts +0 -1
- package/build/tools/trace/resolveSaveStoragePath.test.js +0 -17
- package/build/tools/trace/resolveSessionPaths.test.d.ts +0 -1
- package/build/tools/trace/resolveSessionPaths.test.js +0 -104
- package/build/tools/trace/sessionState.test.d.ts +0 -1
- package/build/tools/trace/sessionState.test.js +0 -17
- package/build/tools/workspace/initializeWorkspaceTool.test.d.ts +0 -1
- package/build/tools/workspace/initializeWorkspaceTool.test.js +0 -139
- package/build/tools/workspace/serviceUpsert.test.d.ts +0 -1
- package/build/tools/workspace/serviceUpsert.test.js +0 -50
- package/build/utils/AnalysisStateManager.test.d.ts +0 -1
- package/build/utils/AnalysisStateManager.test.js +0 -134
- package/build/utils/dartRouteExtractor.test.d.ts +0 -1
- package/build/utils/dartRouteExtractor.test.js +0 -307
- package/build/utils/docker.test.d.ts +0 -1
- package/build/utils/docker.test.js +0 -114
- package/build/utils/featureFlags.test.d.ts +0 -1
- package/build/utils/featureFlags.test.js +0 -81
- package/build/utils/fileWalk.test.d.ts +0 -1
- package/build/utils/fileWalk.test.js +0 -252
- package/build/utils/frontendIntegration.test.d.ts +0 -1
- package/build/utils/frontendIntegration.test.js +0 -229
- package/build/utils/frontendSelectors.test.d.ts +0 -1
- package/build/utils/frontendSelectors.test.js +0 -118
- package/build/utils/gitStaging.test.d.ts +0 -1
- package/build/utils/gitStaging.test.js +0 -111
- package/build/utils/httpDefaults.test.d.ts +0 -1
- package/build/utils/httpDefaults.test.js +0 -21
- package/build/utils/importerHop.test.d.ts +0 -1
- package/build/utils/importerHop.test.js +0 -469
- package/build/utils/pathAffinityClassification.test.d.ts +0 -1
- package/build/utils/pathAffinityClassification.test.js +0 -208
- package/build/utils/planMatchKeys.test.d.ts +0 -1
- package/build/utils/planMatchKeys.test.js +0 -123
- package/build/utils/pom-scope/index.test.d.ts +0 -1
- package/build/utils/pom-scope/index.test.js +0 -278
- package/build/utils/pom-scope/pom-files.test.d.ts +0 -1
- package/build/utils/pom-scope/pom-files.test.js +0 -29
- package/build/utils/pom-scope/scoring.test.d.ts +0 -1
- package/build/utils/pom-scope/scoring.test.js +0 -39
- package/build/utils/pom-scope/selector-extractor.test.d.ts +0 -1
- package/build/utils/pom-scope/selector-extractor.test.js +0 -67
- package/build/utils/pom-verify/bindings.test.d.ts +0 -1
- package/build/utils/pom-verify/bindings.test.js +0 -164
- package/build/utils/pom-verify/calls.test.d.ts +0 -1
- package/build/utils/pom-verify/calls.test.js +0 -61
- package/build/utils/pom-verify/resolve.test.d.ts +0 -1
- package/build/utils/pom-verify/resolve.test.js +0 -114
- package/build/utils/pom-verify/verify.test.d.ts +0 -1
- package/build/utils/pom-verify/verify.test.js +0 -374
- package/build/utils/pr-comment-parser.test.d.ts +0 -1
- package/build/utils/pr-comment-parser.test.js +0 -428
- package/build/utils/progress.test.d.ts +0 -1
- package/build/utils/progress.test.js +0 -37
- package/build/utils/projectMetadata.test.d.ts +0 -1
- package/build/utils/projectMetadata.test.js +0 -172
- package/build/utils/pythonMountPrefixes.test.d.ts +0 -1
- package/build/utils/pythonMountPrefixes.test.js +0 -113
- package/build/utils/repoScanner.test.d.ts +0 -1
- package/build/utils/repoScanner.test.js +0 -190
- package/build/utils/reportVerification.test.d.ts +0 -1
- package/build/utils/reportVerification.test.js +0 -185
- package/build/utils/routeParsers.test.d.ts +0 -1
- package/build/utils/routeParsers.test.js +0 -1011
- package/build/utils/scenarioDrafting.test.d.ts +0 -1
- package/build/utils/scenarioDrafting.test.js +0 -876
- package/build/utils/sourceRouteExtractor.test.d.ts +0 -1
- package/build/utils/sourceRouteExtractor.test.js +0 -738
- package/build/utils/telemetry.test.d.ts +0 -1
- package/build/utils/telemetry.test.js +0 -70
- package/build/utils/trace-parser.test.d.ts +0 -6
- package/build/utils/trace-parser.test.js +0 -140
- package/build/utils/uiPageEnumerator.test.d.ts +0 -1
- package/build/utils/uiPageEnumerator.test.js +0 -824
- package/build/utils/utils.test.d.ts +0 -1
- package/build/utils/utils.test.js +0 -103
- package/build/utils/walkerCharacterization.test.d.ts +0 -1
- package/build/utils/walkerCharacterization.test.js +0 -233
- package/build/utils/workspaceAuth.test.d.ts +0 -1
- package/build/utils/workspaceAuth.test.js +0 -260
- package/build/workspace/testSuites.test.d.ts +0 -1
- package/build/workspace/testSuites.test.js +0 -23
- package/build/workspace/workspace.test.d.ts +0 -1
- 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
|
-
});
|