@skyramp/mcp 0.3.2-rc.pom-4 → 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/adapters/jestAdapter.d.ts +14 -0
- package/build/adapters/jestAdapter.js +113 -0
- package/build/adapters/mochaAdapter.d.ts +13 -0
- package/build/adapters/mochaAdapter.js +87 -0
- package/build/adapters/playwrightAdapter.d.ts +17 -0
- package/build/adapters/playwrightAdapter.js +182 -0
- package/build/adapters/pytestAdapter.d.ts +15 -0
- package/build/adapters/pytestAdapter.js +108 -0
- 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 +82 -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/prompt-utils.d.ts +8 -0
- package/build/prompts/prompt-utils.js +33 -0
- package/build/prompts/sut-setup/modes/adaptWorkflowPrompt.js +70 -7
- package/build/prompts/sut-setup/modes/dockerComposePrompt.js +1 -1
- package/build/prompts/sut-setup/shared.js +19 -17
- package/build/prompts/test-maintenance/drift-analysis-prompt.d.ts +10 -1
- package/build/prompts/test-maintenance/drift-analysis-prompt.js +54 -1
- package/build/prompts/test-recommendation/analysisOutputPrompt.js +21 -29
- package/build/prompts/test-recommendation/scopeAssessment.d.ts +5 -2
- package/build/prompts/test-recommendation/scopeAssessment.js +78 -6
- 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 +39 -55
- 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 +7 -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 +138 -0
- package/build/tools/runExistingTestsTool.js +644 -0
- package/build/tools/submitReportTool.js +30 -1
- package/build/tools/test-management/analyzeChangesTool.d.ts +2 -1
- package/build/tools/test-management/analyzeChangesTool.js +63 -40
- package/build/tools/test-management/analyzeTestHealthTool.d.ts +11 -0
- package/build/tools/test-management/analyzeTestHealthTool.js +63 -1
- package/build/tools/test-management/registerTestPlanTool.js +55 -7
- package/build/tools/trace/startTraceCollectionTool.js +3 -3
- package/build/types/ExternalTestExecution.d.ts +67 -0
- package/build/types/ExternalTestExecution.js +8 -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 +25 -7
- 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 +33 -0
- package/build/utils/AnalysisStateManager.js +36 -2
- 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/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-verify/verify.d.ts +5 -0
- package/build/utils/pom-verify/verify.js +1 -0
- 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/frameworks.d.ts +11 -0
- package/build/workspace/frameworks.js +22 -0
- package/build/workspace/testSuites.d.ts +20 -0
- package/build/workspace/testSuites.js +17 -0
- package/build/workspace/workspace.d.ts +206 -24
- package/build/workspace/workspace.js +52 -2
- package/package.json +5 -2
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mock Compatibility Pre-flight Check
|
|
3
|
+
*
|
|
4
|
+
* Validates mock protocol combinations BEFORE deployment to surface
|
|
5
|
+
* known incompatibilities early rather than at test execution time.
|
|
6
|
+
*/
|
|
7
|
+
import { MockProtocol } from "../types/TestTypes.js";
|
|
8
|
+
export { MockProtocol };
|
|
9
|
+
function parseRestHostname(endpointURL) {
|
|
10
|
+
try {
|
|
11
|
+
const hostname = new URL(endpointURL).hostname.toLowerCase();
|
|
12
|
+
return hostname.startsWith("[") && hostname.endsWith("]")
|
|
13
|
+
? hostname.slice(1, -1)
|
|
14
|
+
: hostname;
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
function isLoopbackRestEndpoint(endpointURL) {
|
|
21
|
+
const hostname = parseRestHostname(endpointURL);
|
|
22
|
+
return (hostname === "localhost" ||
|
|
23
|
+
hostname === "127.0.0.1" ||
|
|
24
|
+
hostname === "0.0.0.0" ||
|
|
25
|
+
hostname === "::1" ||
|
|
26
|
+
hostname === "host.docker.internal");
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Validates a set of mock specs for known protocol compatibility issues.
|
|
30
|
+
*
|
|
31
|
+
* Rules:
|
|
32
|
+
* - Any gRPC mock present → GRPC_PERSISTENT_CONN warning (DNS hijacking skips persistent conns)
|
|
33
|
+
* - Docker local-dev REST mock using loopback/host.docker.internal → REST_LOOPBACK_URL blocking warning
|
|
34
|
+
*
|
|
35
|
+
* REST mocks work correctly in Docker local-dev mode only when the mock URL uses
|
|
36
|
+
* the real Docker service hostname (e.g. "billing" from "http://billing:5001").
|
|
37
|
+
* The worker adds that hostname as a Docker network alias so DNS resolves to the mock server.
|
|
38
|
+
*
|
|
39
|
+
* `compatible` is false when any warning is blocking and the mock cannot intercept traffic.
|
|
40
|
+
* `hasBlockers` is true when at least one warning is blocking (mock must not be generated/deployed).
|
|
41
|
+
*/
|
|
42
|
+
export function validateMockCompatibility(mocks, options = {}) {
|
|
43
|
+
const warnings = [];
|
|
44
|
+
const restMocks = mocks.filter((m) => m.protocol === MockProtocol.Rest);
|
|
45
|
+
const grpcMocks = mocks.filter((m) => m.protocol === MockProtocol.Grpc);
|
|
46
|
+
const loopbackRestMocks = options.dockerLocalDev
|
|
47
|
+
? restMocks.filter((m) => isLoopbackRestEndpoint(m.endpointURL))
|
|
48
|
+
: [];
|
|
49
|
+
if (loopbackRestMocks.length > 0) {
|
|
50
|
+
warnings.push({
|
|
51
|
+
code: "REST_LOOPBACK_URL",
|
|
52
|
+
message: "REST mocks for Docker local-dev must use the original Docker service hostname. " +
|
|
53
|
+
"Loopback hosts such as localhost, 127.0.0.1, 0.0.0.0, ::1, or host.docker.internal " +
|
|
54
|
+
"do not give the Skyramp worker the downstream service DNS alias it needs to intercept traffic.",
|
|
55
|
+
suggestion: "Use the real Docker service origin in endpointURL, for example http://ams:4000 or " +
|
|
56
|
+
"http://billing:5001. Do not use localhost, host.docker.internal, or the Skyramp worker address " +
|
|
57
|
+
"for deployable REST mocks.",
|
|
58
|
+
affectedMocks: loopbackRestMocks.map((m) => m.endpointURL),
|
|
59
|
+
blocking: true,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
// Rule: gRPC mocks use DNS hijacking that doesn't intercept persistent connections
|
|
63
|
+
if (grpcMocks.length > 0) {
|
|
64
|
+
warnings.push({
|
|
65
|
+
code: "GRPC_PERSISTENT_CONN",
|
|
66
|
+
message: "gRPC mocks use DNS hijacking which does not intercept persistent connections. " +
|
|
67
|
+
"If the service under test establishes a gRPC connection before mocks are applied, " +
|
|
68
|
+
"the existing connection will bypass the mock.",
|
|
69
|
+
suggestion: "Apply gRPC mocks before the service under test starts, or restart " +
|
|
70
|
+
"the service after applying mocks so it establishes a new connection through the mock DNS. " +
|
|
71
|
+
"For Docker local-dev, also verify the worker has the original downstream hostname alias " +
|
|
72
|
+
"from endpointURL (for example, partner-accounts for partner-accounts:50051).",
|
|
73
|
+
affectedMocks: grpcMocks.map((m) => m.grpcServiceName || m.endpointURL),
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
const hasBlockers = warnings.some((w) => w.blocking);
|
|
77
|
+
return {
|
|
78
|
+
compatible: !hasBlockers,
|
|
79
|
+
hasBlockers,
|
|
80
|
+
warnings,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
@@ -10,5 +10,10 @@ export type VerifyResult = {
|
|
|
10
10
|
unverifiable: string[];
|
|
11
11
|
parseError?: string;
|
|
12
12
|
checkedCalls: number;
|
|
13
|
+
/** Every POM call found in the spec, verifiable or not. NOT checkedCalls +
|
|
14
|
+
* unverifiable.length: `unverifiable` also absorbs `unrecognized` bindings,
|
|
15
|
+
* which are not calls at all, so that sum overcounts. This is the count of
|
|
16
|
+
* POM members the delivered spec actually reuses. */
|
|
17
|
+
reusedCalls: number;
|
|
13
18
|
};
|
|
14
19
|
export declare function verifyReuse(testFile: string, language: string): Promise<VerifyResult>;
|
package/build/utils/progress.js
CHANGED
|
@@ -10,11 +10,16 @@ export function makeProgressReporter(extra) {
|
|
|
10
10
|
return async (progress, total, message) => {
|
|
11
11
|
const progressToken = extra._meta?.progressToken;
|
|
12
12
|
if (progressToken !== undefined) {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
13
|
+
try {
|
|
14
|
+
const notification = {
|
|
15
|
+
method: "notifications/progress",
|
|
16
|
+
params: { progressToken, progress, total, message },
|
|
17
|
+
};
|
|
18
|
+
await extra.sendNotification(notification);
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
// Progress is best-effort; a disconnected client should not fail the tool.
|
|
22
|
+
}
|
|
18
23
|
}
|
|
19
24
|
};
|
|
20
25
|
}
|
|
@@ -334,7 +334,7 @@ export async function closeProxyTerminal() {
|
|
|
334
334
|
terminalPID,
|
|
335
335
|
});
|
|
336
336
|
}
|
|
337
|
-
catch (
|
|
337
|
+
catch (_killErr) {
|
|
338
338
|
// Process is already dead, which is what we want
|
|
339
339
|
logger.debug("Terminal process already terminated", {
|
|
340
340
|
terminalPID,
|
|
@@ -384,14 +384,14 @@ export async function closeProxyTerminal() {
|
|
|
384
384
|
terminalPID,
|
|
385
385
|
});
|
|
386
386
|
}
|
|
387
|
-
catch (
|
|
387
|
+
catch (_killErr) {
|
|
388
388
|
// Process is already dead, which is what we want
|
|
389
389
|
logger.debug("Terminal process already terminated", {
|
|
390
390
|
terminalPID,
|
|
391
391
|
});
|
|
392
392
|
}
|
|
393
393
|
}
|
|
394
|
-
catch (
|
|
394
|
+
catch (_pidErr) {
|
|
395
395
|
// Fallback to regular process kill
|
|
396
396
|
try {
|
|
397
397
|
process.kill(terminalPID, "SIGTERM");
|
|
@@ -59,15 +59,9 @@ export declare function isRouteLikeFile(filePath: string): boolean;
|
|
|
59
59
|
* route that existed in the base branch.
|
|
60
60
|
*/
|
|
61
61
|
export declare function selectRemovalCandidateFiles(diffData: BranchDiffData): string[];
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
* `git show base:<file>`); files that throw are skipped. Returns every endpoint
|
|
66
|
-
* found in the base versions — callers pass the result to
|
|
67
|
-
* `classifyEndpointsByChangedFiles`, whose moved/still-present filter drops any
|
|
68
|
-
* endpoint that survives in the current catalog, leaving only genuine removals.
|
|
69
|
-
*/
|
|
70
|
-
export declare function recoverRemovedEndpointsFromBase(candidateFiles: string[], fetchBaseContent: (file: string) => Promise<string>): Promise<ScannedEndpoint[]>;
|
|
62
|
+
export declare function recoverRemovedEndpointsFromBase(candidateFiles: string[], fetchBaseContent: (file: string) => Promise<string>, repositoryPath?: string, baseBranch?: string): Promise<ScannedEndpoint[]>;
|
|
63
|
+
/** Strip git's `a/` / `b/` diff-path prefix and normalize separators. */
|
|
64
|
+
export declare function normalizeDiffPath(spec: string): string;
|
|
71
65
|
/**
|
|
72
66
|
* Parse a unified diff into the set of new-side (post-change) line numbers that
|
|
73
67
|
* were touched, keyed by normalized file path.
|
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
import
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as os from "os";
|
|
3
|
+
import * as path from "path";
|
|
4
|
+
import { execFileSync } from "child_process";
|
|
5
|
+
import { joinRoutePrefix, resolvePythonMountPrefixes } from "./pythonMountPrefixes.js";
|
|
2
6
|
import { logger } from "./logger.js";
|
|
3
7
|
export function nextjsFileToApiPath(filePath) {
|
|
4
8
|
const pagesMatch = filePath.match(/(?:^|\/)pages\/(api\/.+)\.[jt]sx?$/);
|
|
@@ -577,9 +581,80 @@ export function selectRemovalCandidateFiles(diffData) {
|
|
|
577
581
|
* `classifyEndpointsByChangedFiles`, whose moved/still-present filter drops any
|
|
578
582
|
* endpoint that survives in the current catalog, leaving only genuine removals.
|
|
579
583
|
*/
|
|
580
|
-
|
|
584
|
+
/**
|
|
585
|
+
* Materialize just the `.py` files of a git ref into a fresh temp directory
|
|
586
|
+
* via `git archive` (no full checkout/worktree) — lets `resolvePythonMountPrefixes`
|
|
587
|
+
* run against the BASE tree unmodified. Best-effort: returns null on any
|
|
588
|
+
* failure (shallow clone missing the ref, git/tar unavailable, etc.) so the
|
|
589
|
+
* caller can fall back rather than abort removal recovery.
|
|
590
|
+
*/
|
|
591
|
+
function materializePythonFilesFromRef(repositoryPath, ref) {
|
|
592
|
+
let tmpDir;
|
|
593
|
+
try {
|
|
594
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "skyramp-base-py-"));
|
|
595
|
+
}
|
|
596
|
+
catch {
|
|
597
|
+
return null;
|
|
598
|
+
}
|
|
599
|
+
const archivePath = path.join(tmpDir, "archive.tar");
|
|
600
|
+
try {
|
|
601
|
+
execFileSync("git", ["-C", repositoryPath, "archive", "--format=tar", "-o", archivePath, ref, "--", "*.py"], {
|
|
602
|
+
stdio: ["ignore", "ignore", "ignore"],
|
|
603
|
+
});
|
|
604
|
+
execFileSync("tar", ["-xf", archivePath, "-C", tmpDir], { stdio: ["ignore", "ignore", "ignore"] });
|
|
605
|
+
fs.rmSync(archivePath, { force: true });
|
|
606
|
+
return tmpDir;
|
|
607
|
+
}
|
|
608
|
+
catch (err) {
|
|
609
|
+
logger.warning("Failed to materialize base-branch Python files for mount-prefix resolution — falling back to current working tree", {
|
|
610
|
+
ref,
|
|
611
|
+
error: err instanceof Error ? err.message : String(err),
|
|
612
|
+
});
|
|
613
|
+
try {
|
|
614
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
615
|
+
}
|
|
616
|
+
catch {
|
|
617
|
+
// best-effort cleanup
|
|
618
|
+
}
|
|
619
|
+
return null;
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
export async function recoverRemovedEndpointsFromBase(candidateFiles, fetchBaseContent, repositoryPath, baseBranch) {
|
|
581
623
|
const routeFiles = candidateFiles.filter(isRouteLikeFile);
|
|
582
624
|
const endpointMap = new Map();
|
|
625
|
+
// Cross-file FastAPI mount-prefix resolution, mirroring the main scan
|
|
626
|
+
// (repoScanner.ts). Without it, a route file whose prefix is applied
|
|
627
|
+
// externally (app.include_router(router, prefix=...)) yields unmounted
|
|
628
|
+
// paths here that can never match the fully-mounted current-catalog
|
|
629
|
+
// paths in classifyEndpointsByChangedFiles's "still exists" check — every
|
|
630
|
+
// endpoint in that file gets misreported as removed, not just the one
|
|
631
|
+
// actually deleted (SKYR-4026).
|
|
632
|
+
//
|
|
633
|
+
// Resolved from the BASE tree when possible (via a `.py`-only git-archive
|
|
634
|
+
// extraction), not the current working tree — if a PR also renames the
|
|
635
|
+
// mount prefix itself (not just the recovered route), head-state prefixes
|
|
636
|
+
// would rewrite the recovered base endpoint onto the NEW prefix, which
|
|
637
|
+
// could mask a real removal or misattribute the path. Falls back to the
|
|
638
|
+
// current working tree if base-tree materialization isn't possible (e.g.
|
|
639
|
+
// shallow clone, git/tar unavailable) — still strictly better than no
|
|
640
|
+
// mount-prefix resolution at all.
|
|
641
|
+
let mountPrefixesByFile;
|
|
642
|
+
if (repositoryPath && routeFiles.some((f) => f.endsWith(".py"))) {
|
|
643
|
+
const baseTreeDir = baseBranch ? materializePythonFilesFromRef(repositoryPath, baseBranch) : null;
|
|
644
|
+
try {
|
|
645
|
+
mountPrefixesByFile = resolvePythonMountPrefixes(baseTreeDir ?? repositoryPath);
|
|
646
|
+
}
|
|
647
|
+
finally {
|
|
648
|
+
if (baseTreeDir) {
|
|
649
|
+
try {
|
|
650
|
+
fs.rmSync(baseTreeDir, { recursive: true, force: true });
|
|
651
|
+
}
|
|
652
|
+
catch {
|
|
653
|
+
// best-effort cleanup
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
}
|
|
583
658
|
for (const file of routeFiles) {
|
|
584
659
|
let content;
|
|
585
660
|
try {
|
|
@@ -597,7 +672,7 @@ export async function recoverRemovedEndpointsFromBase(candidateFiles, fetchBaseC
|
|
|
597
672
|
});
|
|
598
673
|
continue;
|
|
599
674
|
}
|
|
600
|
-
for (const ep of parseFileEndpoints(content, file)) {
|
|
675
|
+
for (const ep of parseFileEndpoints(content, file, mountPrefixesByFile?.get(file))) {
|
|
601
676
|
// parseFileEndpoints already normalizes paths to a leading slash.
|
|
602
677
|
const key = `${file}::${ep.path}`;
|
|
603
678
|
const existing = endpointMap.get(key);
|
|
@@ -620,7 +695,7 @@ export async function recoverRemovedEndpointsFromBase(candidateFiles, fetchBaseC
|
|
|
620
695
|
}));
|
|
621
696
|
}
|
|
622
697
|
/** Strip git's `a/` / `b/` diff-path prefix and normalize separators. */
|
|
623
|
-
function normalizeDiffPath(spec) {
|
|
698
|
+
export function normalizeDiffPath(spec) {
|
|
624
699
|
let p = spec.replace(/\\/g, "/");
|
|
625
700
|
if (p.startsWith("a/") || p.startsWith("b/"))
|
|
626
701
|
p = p.slice(2);
|
package/build/utils/utils.js
CHANGED
|
@@ -69,7 +69,7 @@ export function getPathParameterValidationError(requiredPathParams) {
|
|
|
69
69
|
return (`Please specify value for the following required path parameter:\n` +
|
|
70
70
|
`${requiredPathParams
|
|
71
71
|
.split(",")
|
|
72
|
-
.map((param
|
|
72
|
+
.map((param) => `- \`${param.trim()}\``)
|
|
73
73
|
.join("\n")}\n\n`);
|
|
74
74
|
}
|
|
75
75
|
}
|
|
@@ -150,7 +150,7 @@ export function validateRequestData(value) {
|
|
|
150
150
|
JSON.parse(value);
|
|
151
151
|
return value;
|
|
152
152
|
}
|
|
153
|
-
catch (
|
|
153
|
+
catch (_e) {
|
|
154
154
|
logger.warning("Request data is not a valid JSON string", { value });
|
|
155
155
|
}
|
|
156
156
|
const isAbsolute = path.isAbsolute(value);
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
-
export declare const SKYRAMP_IMAGE_VERSION = "v1.3.
|
|
2
|
-
export declare const EXECUTOR_DOCKER_IMAGE = "skyramp/executor:v1.3.
|
|
3
|
-
export declare const WORKER_DOCKER_IMAGE = "skyramp/worker:v1.3.
|
|
1
|
+
export declare const SKYRAMP_IMAGE_VERSION = "v1.3.37";
|
|
2
|
+
export declare const EXECUTOR_DOCKER_IMAGE = "skyramp/executor:v1.3.37";
|
|
3
|
+
export declare const WORKER_DOCKER_IMAGE = "skyramp/worker:v1.3.37";
|
|
4
|
+
export declare const WORKER_CONTROL_PORT = 35142;
|
package/build/utils/versions.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
-
export const SKYRAMP_IMAGE_VERSION = "v1.3.
|
|
1
|
+
export const SKYRAMP_IMAGE_VERSION = "v1.3.37";
|
|
2
2
|
export const EXECUTOR_DOCKER_IMAGE = `skyramp/executor:${SKYRAMP_IMAGE_VERSION}`;
|
|
3
3
|
export const WORKER_DOCKER_IMAGE = `skyramp/worker:${SKYRAMP_IMAGE_VERSION}`;
|
|
4
|
+
// Control port the Skyramp worker listens on (SDK `CONTAINER_PORT`).
|
|
5
|
+
export const WORKER_CONTROL_PORT = 35142;
|
|
@@ -133,3 +133,49 @@ export declare function getWorkspaceAuthConfig(repositoryPath: string): Promise<
|
|
|
133
133
|
/** True when any workspace service declares api.skipTLSVerify — the SUT serves
|
|
134
134
|
* self-signed TLS and generated tests must skip cert verification (SKYR-3961). */
|
|
135
135
|
export declare function getWorkspaceSkipTLSVerify(repositoryPath: string): Promise<boolean>;
|
|
136
|
+
/**
|
|
137
|
+
* Resolve api.defaultQueryParams for test generation — query params always
|
|
138
|
+
* attached to every generated request for this service (e.g. RBAC context
|
|
139
|
+
* an authorization layer requires beyond the auth header). Same
|
|
140
|
+
* first-matching-service convention as getWorkspaceAuthConfig (SKYR-4050).
|
|
141
|
+
*
|
|
142
|
+
* Unlike readWorkspaceConfigRaw (which delegates to WorkspaceConfigManager and
|
|
143
|
+
* resolves EXACTLY `<repositoryPath>/.skyramp/workspace.yml`), this walks up
|
|
144
|
+
* from `repositoryPath` to find the nearest `.skyramp/workspace.yml` — both
|
|
145
|
+
* call sites pass a nested test-output directory (e.g. `<repo>/tests/skyramp`),
|
|
146
|
+
* not the repo root, so an exact-match lookup would silently never find the
|
|
147
|
+
* config. Mirrors the sync fs.existsSync walk-up in
|
|
148
|
+
* generateBatchScenarioRestTool.ts.
|
|
149
|
+
*/
|
|
150
|
+
/**
|
|
151
|
+
* Walk up from `startDir` to find the nearest `.skyramp/workspace.yml`, checking
|
|
152
|
+
* `startDir` itself first and continuing through every ancestor INCLUDING the
|
|
153
|
+
* filesystem root — a naive `while (dir !== path.dirname(dir))` loop exits
|
|
154
|
+
* before ever checking the root candidate, since that equality only becomes
|
|
155
|
+
* true once `dir` already IS the root. Returns the full config path, or null.
|
|
156
|
+
* `existsSync` is injectable for testing; defaults to the real filesystem.
|
|
157
|
+
*/
|
|
158
|
+
export declare function findWorkspaceConfigPath(startDir: string, existsSync?: (p: string) => boolean): string | null;
|
|
159
|
+
export declare function getWorkspaceDefaultQueryParams(repositoryPath: string): Promise<Record<string, string> | undefined>;
|
|
160
|
+
/**
|
|
161
|
+
* Merge workspace-declared default query params into a comma-separated
|
|
162
|
+
* "key=value,key2=value2" queryParams string (the format used by
|
|
163
|
+
* baseTestSchema.queryParams). Existing keys always win on collision.
|
|
164
|
+
*/
|
|
165
|
+
export declare function mergeQueryParamsString(existing: string | undefined, defaults: Record<string, string>): string;
|
|
166
|
+
/**
|
|
167
|
+
* Merge workspace-declared default query params into a JSON-object queryParams
|
|
168
|
+
* string (the format used by batch-scenario step queryParams). Existing keys
|
|
169
|
+
* always win on collision. A malformed existingJson is left untouched — the
|
|
170
|
+
* caller's own JSON.parse warning path still fires downstream.
|
|
171
|
+
*/
|
|
172
|
+
export declare function mergeQueryParamsJson(existingJson: string | undefined, defaults: Record<string, string>): string;
|
|
173
|
+
/**
|
|
174
|
+
* Resolve auth header and scheme from workspace config for test generation.
|
|
175
|
+
* Returns null when auth should be skipped (authType absent or "none").
|
|
176
|
+
* Single source of truth — used by both TestGenerationService and batch scenario tool.
|
|
177
|
+
*/
|
|
178
|
+
export declare function resolveAuthFromWorkspace(repoPath: string, existingAuthHeader?: string, existingAuthScheme?: string): Promise<{
|
|
179
|
+
authHeader: string;
|
|
180
|
+
authScheme?: string;
|
|
181
|
+
} | null>;
|
|
@@ -89,7 +89,20 @@ export async function getWorkspaceBaseUrl(repositoryPath, testFile, language, te
|
|
|
89
89
|
return absTestFile.startsWith(absTestDirectory + path.sep) || absTestFile === absTestDirectory;
|
|
90
90
|
});
|
|
91
91
|
});
|
|
92
|
-
// Step 2:
|
|
92
|
+
// Step 2: longest-prefix match — when multiple services match (e.g.
|
|
93
|
+
// tests/skyramp is a prefix of tests/skyramp/cart), pick the service
|
|
94
|
+
// whose testDirectory is the most specific (longest) prefix of the test file.
|
|
95
|
+
if (matches.length > 1) {
|
|
96
|
+
matches.sort((a, b) => {
|
|
97
|
+
const aLen = path.resolve(repositoryPath, a.testDirectory).length;
|
|
98
|
+
const bLen = path.resolve(repositoryPath, b.testDirectory).length;
|
|
99
|
+
return bLen - aLen; // longest first
|
|
100
|
+
});
|
|
101
|
+
// Keep only services with the longest matching testDirectory length.
|
|
102
|
+
const longestLen = path.resolve(repositoryPath, matches[0].testDirectory).length;
|
|
103
|
+
matches = matches.filter((s) => path.resolve(repositoryPath, s.testDirectory).length === longestLen);
|
|
104
|
+
}
|
|
105
|
+
// Step 3: narrow by language if still ambiguous, but don't eliminate all matches
|
|
93
106
|
if (matches.length > 1 && language) {
|
|
94
107
|
const byLanguage = matches.filter((s) => s.language?.toLowerCase() === language.toLowerCase());
|
|
95
108
|
if (byLanguage.length > 0)
|
|
@@ -350,3 +363,145 @@ export async function getWorkspaceSkipTLSVerify(repositoryPath) {
|
|
|
350
363
|
return false;
|
|
351
364
|
return (rawConfig.services ?? []).some((s) => s.api?.skipTLSVerify === true);
|
|
352
365
|
}
|
|
366
|
+
/**
|
|
367
|
+
* Resolve api.defaultQueryParams for test generation — query params always
|
|
368
|
+
* attached to every generated request for this service (e.g. RBAC context
|
|
369
|
+
* an authorization layer requires beyond the auth header). Same
|
|
370
|
+
* first-matching-service convention as getWorkspaceAuthConfig (SKYR-4050).
|
|
371
|
+
*
|
|
372
|
+
* Unlike readWorkspaceConfigRaw (which delegates to WorkspaceConfigManager and
|
|
373
|
+
* resolves EXACTLY `<repositoryPath>/.skyramp/workspace.yml`), this walks up
|
|
374
|
+
* from `repositoryPath` to find the nearest `.skyramp/workspace.yml` — both
|
|
375
|
+
* call sites pass a nested test-output directory (e.g. `<repo>/tests/skyramp`),
|
|
376
|
+
* not the repo root, so an exact-match lookup would silently never find the
|
|
377
|
+
* config. Mirrors the sync fs.existsSync walk-up in
|
|
378
|
+
* generateBatchScenarioRestTool.ts.
|
|
379
|
+
*/
|
|
380
|
+
/**
|
|
381
|
+
* Walk up from `startDir` to find the nearest `.skyramp/workspace.yml`, checking
|
|
382
|
+
* `startDir` itself first and continuing through every ancestor INCLUDING the
|
|
383
|
+
* filesystem root — a naive `while (dir !== path.dirname(dir))` loop exits
|
|
384
|
+
* before ever checking the root candidate, since that equality only becomes
|
|
385
|
+
* true once `dir` already IS the root. Returns the full config path, or null.
|
|
386
|
+
* `existsSync` is injectable for testing; defaults to the real filesystem.
|
|
387
|
+
*/
|
|
388
|
+
export function findWorkspaceConfigPath(startDir, existsSync = fs.existsSync) {
|
|
389
|
+
const WS_SUBPATH = path.join(".skyramp", "workspace.yml");
|
|
390
|
+
let searchDir = path.resolve(startDir);
|
|
391
|
+
while (true) {
|
|
392
|
+
const candidate = path.join(searchDir, WS_SUBPATH);
|
|
393
|
+
if (existsSync(candidate))
|
|
394
|
+
return candidate;
|
|
395
|
+
const parentDir = path.dirname(searchDir);
|
|
396
|
+
if (parentDir === searchDir)
|
|
397
|
+
return null; // reached the filesystem root; already checked above
|
|
398
|
+
searchDir = parentDir;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
export async function getWorkspaceDefaultQueryParams(repositoryPath) {
|
|
402
|
+
const wsConfigPath = findWorkspaceConfigPath(repositoryPath);
|
|
403
|
+
if (!wsConfigPath)
|
|
404
|
+
return undefined;
|
|
405
|
+
try {
|
|
406
|
+
const raw = fs.readFileSync(wsConfigPath, "utf-8");
|
|
407
|
+
const parsed = yaml.load(raw);
|
|
408
|
+
const svc = parsed?.services?.find((s) => s.api?.defaultQueryParams !== undefined);
|
|
409
|
+
return svc?.api?.defaultQueryParams;
|
|
410
|
+
}
|
|
411
|
+
catch {
|
|
412
|
+
return undefined;
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
/**
|
|
416
|
+
* Merge workspace-declared default query params into a comma-separated
|
|
417
|
+
* "key=value,key2=value2" queryParams string (the format used by
|
|
418
|
+
* baseTestSchema.queryParams). Existing keys always win on collision.
|
|
419
|
+
*/
|
|
420
|
+
export function mergeQueryParamsString(existing, defaults) {
|
|
421
|
+
const pairs = new Map();
|
|
422
|
+
if (existing) {
|
|
423
|
+
for (const pair of existing.split(",")) {
|
|
424
|
+
const eqIndex = pair.indexOf("=");
|
|
425
|
+
if (eqIndex === -1)
|
|
426
|
+
continue;
|
|
427
|
+
const key = pair.slice(0, eqIndex).trim();
|
|
428
|
+
const value = pair.slice(eqIndex + 1).trim();
|
|
429
|
+
if (key)
|
|
430
|
+
pairs.set(key, value);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
for (const [key, value] of Object.entries(defaults)) {
|
|
434
|
+
if (typeof value !== "string") {
|
|
435
|
+
logger.warning("Skipping workspace defaultQueryParams entry with a non-string value", { key });
|
|
436
|
+
continue;
|
|
437
|
+
}
|
|
438
|
+
if (key.includes(",") || key.includes("=") || value.includes(",") || value.includes("=")) {
|
|
439
|
+
logger.warning("Skipping workspace defaultQueryParams entry with invalid characters", { key });
|
|
440
|
+
continue;
|
|
441
|
+
}
|
|
442
|
+
if (!pairs.has(key))
|
|
443
|
+
pairs.set(key, value);
|
|
444
|
+
}
|
|
445
|
+
return Array.from(pairs.entries())
|
|
446
|
+
.map(([key, value]) => `${key}=${value}`)
|
|
447
|
+
.join(",");
|
|
448
|
+
}
|
|
449
|
+
/**
|
|
450
|
+
* Merge workspace-declared default query params into a JSON-object queryParams
|
|
451
|
+
* string (the format used by batch-scenario step queryParams). Existing keys
|
|
452
|
+
* always win on collision. A malformed existingJson is left untouched — the
|
|
453
|
+
* caller's own JSON.parse warning path still fires downstream.
|
|
454
|
+
*/
|
|
455
|
+
export function mergeQueryParamsJson(existingJson, defaults) {
|
|
456
|
+
let parsed = {};
|
|
457
|
+
if (existingJson) {
|
|
458
|
+
try {
|
|
459
|
+
const candidate = JSON.parse(existingJson);
|
|
460
|
+
if (!candidate || typeof candidate !== "object" || Array.isArray(candidate)) {
|
|
461
|
+
return existingJson;
|
|
462
|
+
}
|
|
463
|
+
parsed = candidate;
|
|
464
|
+
}
|
|
465
|
+
catch {
|
|
466
|
+
return existingJson;
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
return JSON.stringify({ ...defaults, ...parsed });
|
|
470
|
+
}
|
|
471
|
+
/**
|
|
472
|
+
* Resolve auth header and scheme from workspace config for test generation.
|
|
473
|
+
* Returns null when auth should be skipped (authType absent or "none").
|
|
474
|
+
* Single source of truth — used by both TestGenerationService and batch scenario tool.
|
|
475
|
+
*/
|
|
476
|
+
export async function resolveAuthFromWorkspace(repoPath, existingAuthHeader, existingAuthScheme) {
|
|
477
|
+
if (existingAuthHeader !== undefined)
|
|
478
|
+
return null; // caller already specified
|
|
479
|
+
const wsAuth = await getWorkspaceAuthConfig(repoPath);
|
|
480
|
+
// Skip auth when explicitly none. When authType is absent, still resolve if
|
|
481
|
+
// authHeader or authScheme is set (some workspaces specify only the header).
|
|
482
|
+
if (wsAuth.authType === WorkspaceAuthType.None)
|
|
483
|
+
return null;
|
|
484
|
+
if (!wsAuth.authType && !wsAuth.authHeader && !wsAuth.authScheme)
|
|
485
|
+
return null;
|
|
486
|
+
let resolvedHeader = wsAuth.authHeader ?? getDefaultAuthHeader(wsAuth.authType);
|
|
487
|
+
if (!resolvedHeader && wsAuth.authScheme !== undefined) {
|
|
488
|
+
resolvedHeader = "Authorization";
|
|
489
|
+
}
|
|
490
|
+
if (wsAuth.authType === WorkspaceAuthType.ApiKey && !resolvedHeader) {
|
|
491
|
+
logger.warning("workspace.yml has authType: apiKey but no authHeader is set — " +
|
|
492
|
+
"requests will be sent without an auth header and will likely receive 401/403. " +
|
|
493
|
+
"Add authHeader: <X-Your-Key-Header> to .skyramp/workspace.yml.");
|
|
494
|
+
return null;
|
|
495
|
+
}
|
|
496
|
+
if (!resolvedHeader)
|
|
497
|
+
return null;
|
|
498
|
+
logger.info("authHeader resolved from workspace config", {
|
|
499
|
+
authHeader: resolvedHeader,
|
|
500
|
+
authType: wsAuth.authType,
|
|
501
|
+
});
|
|
502
|
+
let authScheme = existingAuthScheme;
|
|
503
|
+
if (isAuthorizationHeaderName(resolvedHeader)) {
|
|
504
|
+
authScheme = authScheme ?? wsAuth.authScheme ?? getAuthScheme(wsAuth.authType);
|
|
505
|
+
}
|
|
506
|
+
return { authHeader: resolvedHeader, authScheme };
|
|
507
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical set of supported test frameworks — the single source of truth for
|
|
3
|
+
* both the workspace `framework` enum and the `framework` tool-param description
|
|
4
|
+
* in TestTypes.ts. `robot` is Robot Framework (Python). Extend this list when a
|
|
5
|
+
* new framework is genuinely supported; do not fork it.
|
|
6
|
+
*
|
|
7
|
+
* Kept in its own dependency-free module so lightweight consumers (e.g.
|
|
8
|
+
* TestTypes.ts) can read the list without importing workspace.ts and pulling in
|
|
9
|
+
* its Node-only dependencies (fs / child_process / js-yaml).
|
|
10
|
+
*/
|
|
11
|
+
export declare const SUPPORTED_FRAMEWORKS: readonly ["playwright", "pytest", "robot", "junit", "vitest", "jest", "mocha", "go", "rspec", "jasmine"];
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical set of supported test frameworks — the single source of truth for
|
|
3
|
+
* both the workspace `framework` enum and the `framework` tool-param description
|
|
4
|
+
* in TestTypes.ts. `robot` is Robot Framework (Python). Extend this list when a
|
|
5
|
+
* new framework is genuinely supported; do not fork it.
|
|
6
|
+
*
|
|
7
|
+
* Kept in its own dependency-free module so lightweight consumers (e.g.
|
|
8
|
+
* TestTypes.ts) can read the list without importing workspace.ts and pulling in
|
|
9
|
+
* its Node-only dependencies (fs / child_process / js-yaml).
|
|
10
|
+
*/
|
|
11
|
+
export const SUPPORTED_FRAMEWORKS = [
|
|
12
|
+
"playwright",
|
|
13
|
+
"pytest",
|
|
14
|
+
"robot",
|
|
15
|
+
"junit",
|
|
16
|
+
"vitest",
|
|
17
|
+
"jest",
|
|
18
|
+
"mocha",
|
|
19
|
+
"go",
|
|
20
|
+
"rspec",
|
|
21
|
+
"jasmine",
|
|
22
|
+
];
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { Service } from "./workspace.js";
|
|
2
|
+
/**
|
|
3
|
+
* A single test suite (one runtimeDetails.testSuites[] entry) flattened to one
|
|
4
|
+
* uniform shape, carrying its owning serviceName so routing/aggregation can label
|
|
5
|
+
* results per suite, and a framework resolved against the service's.
|
|
6
|
+
*/
|
|
7
|
+
export interface NormalizedSuite {
|
|
8
|
+
serviceName: string;
|
|
9
|
+
framework?: string;
|
|
10
|
+
testRunCommand: string;
|
|
11
|
+
testEnv?: Record<string, string>;
|
|
12
|
+
/** Repo-relative globs this suite owns, for selector→suite routing. */
|
|
13
|
+
pathGlobs?: string[];
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Normalizes a service's runtimeDetails.testSuites into a flat list of suites
|
|
17
|
+
* (SKYR-4002), each carrying its owning serviceName and a framework that falls
|
|
18
|
+
* back to the service's. Returns [] when the service records no testSuites.
|
|
19
|
+
*/
|
|
20
|
+
export declare function resolveSuites(service: Service): NormalizedSuite[];
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Normalizes a service's runtimeDetails.testSuites into a flat list of suites
|
|
3
|
+
* (SKYR-4002), each carrying its owning serviceName and a framework that falls
|
|
4
|
+
* back to the service's. Returns [] when the service records no testSuites.
|
|
5
|
+
*/
|
|
6
|
+
export function resolveSuites(service) {
|
|
7
|
+
const suites = service.runtimeDetails?.testSuites;
|
|
8
|
+
if (!suites)
|
|
9
|
+
return [];
|
|
10
|
+
return suites.map((suite) => ({
|
|
11
|
+
serviceName: service.serviceName,
|
|
12
|
+
framework: suite.framework ?? service.framework,
|
|
13
|
+
testRunCommand: suite.testRunCommand,
|
|
14
|
+
testEnv: suite.testEnv,
|
|
15
|
+
pathGlobs: suite.pathGlobs,
|
|
16
|
+
}));
|
|
17
|
+
}
|