@skyramp/mcp 0.3.0 → 0.3.1-ldw-beta
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/commandLibrary.test.d.ts +1 -0
- package/build/commands/commandLibrary.test.js +59 -0
- package/build/commands/localDevTestChangesCommand.d.ts +15 -0
- package/build/commands/localDevTestChangesCommand.js +201 -0
- package/build/index.js +80 -6
- package/build/playwright/traceRecordingPrompt.js +2 -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 +466 -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/test-recommendation/analysisOutputPrompt.js +21 -29
- package/build/prompts/test-recommendation/analysisOutputPrompt.test.js +0 -28
- package/build/prompts/test-recommendation/test-recommendation-prompt.js +41 -4
- package/build/prompts/test-recommendation/test-recommendation-prompt.test.js +16 -1
- package/build/prompts/testbot/testbot-prompts.d.ts +0 -5
- package/build/prompts/testbot/testbot-prompts.js +5 -34
- 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/TestExecutionService.test.js +83 -3
- package/build/services/TestGenerationService.d.ts +2 -2
- package/build/services/TestGenerationService.js +7 -21
- package/build/services/containerEnv.js +3 -1
- package/build/tool-phases.js +6 -0
- package/build/tools/code-refactor/enhanceAssertionsTool.js +47 -18
- package/build/tools/enrichTestWithMocksTool.d.ts +28 -0
- package/build/tools/enrichTestWithMocksTool.js +726 -0
- package/build/tools/enrichTestWithMocksTool.test.d.ts +1 -0
- package/build/tools/enrichTestWithMocksTool.test.js +266 -0
- package/build/tools/executeSkyrampTestTool.d.ts +11 -0
- package/build/tools/executeSkyrampTestTool.js +63 -22
- package/build/tools/executeSkyrampTestTool.test.d.ts +1 -0
- package/build/tools/executeSkyrampTestTool.test.js +36 -0
- package/build/tools/generate-tests/batchMockGenerationTool.d.ts +85 -0
- package/build/tools/generate-tests/batchMockGenerationTool.js +432 -0
- package/build/tools/generate-tests/generateBatchScenarioRestTool.js +6 -21
- package/build/tools/generate-tests/generateBatchScenarioRestTool.test.js +1 -0
- package/build/tools/generate-tests/generateContractRestTool.js +2 -2
- package/build/tools/generate-tests/generateMockRestTool.d.ts +129 -6
- package/build/tools/generate-tests/generateMockRestTool.js +234 -22
- package/build/tools/generate-tests/loadTestSchema.d.ts +1 -1
- package/build/tools/generateEnrichedIntegrationTestTool.d.ts +25 -0
- package/build/tools/generateEnrichedIntegrationTestTool.js +235 -0
- package/build/tools/generateEnrichedIntegrationTestTool.test.d.ts +1 -0
- package/build/tools/generateEnrichedIntegrationTestTool.test.js +44 -0
- package/build/tools/localDevWorkerComposeTool.d.ts +24 -0
- package/build/tools/localDevWorkerComposeTool.js +224 -0
- package/build/tools/localDevWorkerComposeTool.test.d.ts +1 -0
- package/build/tools/localDevWorkerComposeTool.test.js +129 -0
- package/build/tools/localDevWorkflowFixes.test.d.ts +1 -0
- package/build/tools/localDevWorkflowFixes.test.js +255 -0
- package/build/tools/one-click/oneClickTool.d.ts +13 -0
- package/build/tools/one-click/oneClickTool.js +195 -24
- package/build/tools/one-click/oneClickTool.test.d.ts +1 -0
- package/build/tools/one-click/oneClickTool.test.js +172 -0
- 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/queryProxyMocksTool.test.d.ts +1 -0
- package/build/tools/queryProxyMocksTool.test.js +164 -0
- package/build/tools/test-management/analyzeChangesTool.d.ts +2 -1
- package/build/tools/test-management/analyzeChangesTool.js +61 -38
- package/build/tools/trace/startTraceCollectionTool.js +3 -3
- package/build/types/OneClickCommands.d.ts +1 -1
- package/build/types/RepositoryAnalysis.d.ts +117 -0
- package/build/types/RepositoryAnalysis.js +16 -2
- package/build/types/TestExecution.d.ts +1 -0
- package/build/types/TestTypes.d.ts +25 -7
- package/build/types/TestTypes.js +19 -5
- package/build/utils/analyze-openapi.js +18 -1
- package/build/utils/analyze-openapi.test.d.ts +1 -0
- package/build/utils/analyze-openapi.test.js +19 -0
- package/build/utils/branchDiff.d.ts +17 -1
- package/build/utils/branchDiff.js +99 -14
- package/build/utils/branchDiff.test.d.ts +1 -0
- package/build/utils/branchDiff.test.js +109 -0
- package/build/utils/featureFlags.d.ts +16 -0
- package/build/utils/featureFlags.js +20 -0
- package/build/utils/featureFlags.test.js +57 -2
- package/build/utils/grpcMockValidation.d.ts +1 -0
- package/build/utils/grpcMockValidation.js +49 -0
- package/build/utils/grpcMockValidation.test.d.ts +1 -0
- package/build/utils/grpcMockValidation.test.js +41 -0
- package/build/utils/httpMethodValidation.d.ts +4 -0
- package/build/utils/httpMethodValidation.js +15 -0
- package/build/utils/mockCompatibility.d.ts +49 -0
- package/build/utils/mockCompatibility.js +82 -0
- package/build/utils/mockCompatibility.test.d.ts +1 -0
- package/build/utils/mockCompatibility.test.js +79 -0
- package/build/utils/progress.js +10 -5
- package/build/utils/versions.d.ts +1 -0
- package/build/utils/versions.js +2 -0
- package/build/utils/workspaceAuth.d.ts +9 -0
- package/build/utils/workspaceAuth.js +51 -1
- package/build/workspace/workspace.d.ts +28 -14
- package/build/workspace/workspace.js +3 -1
- package/node_modules/playwright/lib/mcp/browser/tools/screenshot.js +1 -1
- package/node_modules/playwright/lib/mcp/skyramp/resultCode.js +45 -0
- package/node_modules/playwright/lib/mcp/skyramp/traceRecordingBackend.js +6 -22
- package/node_modules/playwright/lib/mcp/test/resultCode.test.js +48 -0
- package/node_modules/playwright/node_modules/playwright-core/package.json +1 -1
- package/node_modules/playwright/package.json +1 -1
- package/package.json +2 -2
|
@@ -14,6 +14,9 @@ export function parseChangedFilesFromDiff(rawDiff) {
|
|
|
14
14
|
return out;
|
|
15
15
|
}
|
|
16
16
|
/** Parse diff headers to find newly created and deleted files. */
|
|
17
|
+
function isShaLikeRef(ref) {
|
|
18
|
+
return /^[0-9a-f]{7,40}$/i.test(ref);
|
|
19
|
+
}
|
|
17
20
|
function parseNewAndDeletedFiles(rawDiff) {
|
|
18
21
|
const newFiles = [];
|
|
19
22
|
const deletedFiles = [];
|
|
@@ -34,24 +37,87 @@ function parseNewAndDeletedFiles(rawDiff) {
|
|
|
34
37
|
}
|
|
35
38
|
return { newFiles, deletedFiles };
|
|
36
39
|
}
|
|
37
|
-
|
|
40
|
+
/**
|
|
41
|
+
* Normalize and validate a caller-provided base branch/ref before passing it
|
|
42
|
+
* to git. Reject option-like or ambiguous values so the ref is always treated
|
|
43
|
+
* as a revision name, not as a command-line flag.
|
|
44
|
+
*/
|
|
45
|
+
export function normalizeProvidedBaseRef(providedBaseBranch) {
|
|
46
|
+
if (providedBaseBranch === undefined)
|
|
47
|
+
return undefined;
|
|
48
|
+
const trimmed = providedBaseBranch.trim();
|
|
49
|
+
if (!trimmed)
|
|
50
|
+
return undefined;
|
|
51
|
+
if (trimmed.startsWith("-")) {
|
|
52
|
+
throw new Error(`Invalid base branch/ref "${providedBaseBranch}": refs must not start with '-'.`);
|
|
53
|
+
}
|
|
54
|
+
if (/\s/.test(trimmed) || trimmed.includes("\0")) {
|
|
55
|
+
throw new Error(`Invalid base branch/ref "${providedBaseBranch}": refs must not contain whitespace or NUL characters.`);
|
|
56
|
+
}
|
|
57
|
+
if (trimmed.includes("..")) {
|
|
58
|
+
throw new Error(`Invalid base branch/ref "${providedBaseBranch}": range syntax (".."/""..."") is not allowed — provide a single ref.`);
|
|
59
|
+
}
|
|
60
|
+
return trimmed;
|
|
61
|
+
}
|
|
62
|
+
async function tryDiff(git, ref, includeUncommitted = false) {
|
|
38
63
|
try {
|
|
39
|
-
|
|
64
|
+
// When includeUncommitted is true, diff the ref directly against the
|
|
65
|
+
// working tree so uncommitted/unstaged local changes are captured.
|
|
66
|
+
const diffRange = includeUncommitted ? ref : `${ref}...HEAD`;
|
|
67
|
+
const changedFilesRaw = await git.diff([diffRange, "--name-only"]);
|
|
40
68
|
const changedFiles = changedFilesRaw
|
|
41
69
|
.split("\n")
|
|
42
70
|
.map((f) => f.trim())
|
|
43
71
|
.filter((f) => f.length > 0);
|
|
44
|
-
const diffStat = await git.diff([
|
|
45
|
-
const fullDiff = await git.diff([
|
|
72
|
+
const diffStat = await git.diff([diffRange, "--stat"]);
|
|
73
|
+
const fullDiff = await git.diff([diffRange]);
|
|
46
74
|
const { newFiles, deletedFiles } = parseNewAndDeletedFiles(fullDiff);
|
|
47
|
-
|
|
75
|
+
// git diff does not include untracked files. Include them as both changed
|
|
76
|
+
// and new so endpoint classification treats newly created local files as new.
|
|
77
|
+
if (includeUncommitted) {
|
|
78
|
+
const statusResult = await git.status();
|
|
79
|
+
const untrackedFiles = statusResult.not_added ?? [];
|
|
80
|
+
if (untrackedFiles.length > 0) {
|
|
81
|
+
const changedSet = new Set(changedFiles);
|
|
82
|
+
const newSet = new Set(newFiles);
|
|
83
|
+
for (const file of untrackedFiles) {
|
|
84
|
+
if (!changedSet.has(file)) {
|
|
85
|
+
changedFiles.push(file);
|
|
86
|
+
changedSet.add(file);
|
|
87
|
+
}
|
|
88
|
+
if (!newSet.has(file)) {
|
|
89
|
+
newFiles.push(file);
|
|
90
|
+
newSet.add(file);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return {
|
|
96
|
+
changedFiles,
|
|
97
|
+
diffContent: fullDiff,
|
|
98
|
+
diffStat,
|
|
99
|
+
newFiles,
|
|
100
|
+
deletedFiles,
|
|
101
|
+
};
|
|
48
102
|
}
|
|
49
103
|
catch (err) {
|
|
50
|
-
logger.debug(`tryDiff against ${ref} failed`, {
|
|
104
|
+
logger.debug(`tryDiff against ${ref} failed`, {
|
|
105
|
+
error: err instanceof Error ? err.message : String(err),
|
|
106
|
+
});
|
|
51
107
|
return undefined;
|
|
52
108
|
}
|
|
53
109
|
}
|
|
54
|
-
|
|
110
|
+
/**
|
|
111
|
+
* Compute the diff between a base reference and the current state.
|
|
112
|
+
*
|
|
113
|
+
* @param repositoryPath Absolute path to the git repository.
|
|
114
|
+
* @param providedBaseBranch A branch name, tag, or commit SHA to diff against.
|
|
115
|
+
* When omitted, the remote default branch is auto-detected.
|
|
116
|
+
* @param includeUncommitted When true, diffs against the working tree so
|
|
117
|
+
* uncommitted/unstaged changes are included. Defaults to false (CI mode
|
|
118
|
+
* that only considers committed changes via the three-dot form).
|
|
119
|
+
*/
|
|
120
|
+
export async function computeBranchDiff(repositoryPath, providedBaseBranch, includeUncommitted = false) {
|
|
55
121
|
const git = simpleGit(repositoryPath);
|
|
56
122
|
const isRepo = await git.checkIsRepo();
|
|
57
123
|
if (!isRepo) {
|
|
@@ -60,14 +126,32 @@ export async function computeBranchDiff(repositoryPath, providedBaseBranch) {
|
|
|
60
126
|
const branchInfo = await git.branch();
|
|
61
127
|
const currentBranch = branchInfo.current || "HEAD";
|
|
62
128
|
// Build candidate base refs in priority order:
|
|
63
|
-
// 1. Provided
|
|
64
|
-
// 2.
|
|
65
|
-
// 3.
|
|
129
|
+
// 1. Provided value used directly (supports SHAs, local branches, tags)
|
|
130
|
+
// 2. Provided value with origin/ prefix (remote branch)
|
|
131
|
+
// 3. Remote default branch auto-detected from available remotes
|
|
132
|
+
// 4. FETCH_HEAD — present in base_commit repos (git fetch --depth 1 origin <sha>)
|
|
66
133
|
// where no remote tracking branch is created
|
|
67
|
-
//
|
|
134
|
+
// 5. HEAD~1 — last resort for repos with no remote at all
|
|
68
135
|
const candidates = [];
|
|
69
|
-
|
|
70
|
-
|
|
136
|
+
const normalizedBaseBranch = normalizeProvidedBaseRef(providedBaseBranch);
|
|
137
|
+
if (normalizedBaseBranch) {
|
|
138
|
+
const isRemoteQualified = normalizedBaseBranch.startsWith("origin/");
|
|
139
|
+
const looksLikeSimpleBranch = !isRemoteQualified &&
|
|
140
|
+
!isShaLikeRef(normalizedBaseBranch) &&
|
|
141
|
+
!/[~^:@]/.test(normalizedBaseBranch);
|
|
142
|
+
if (includeUncommitted && looksLikeSimpleBranch) {
|
|
143
|
+
// Local-dev diffs should prefer the remote base branch when available;
|
|
144
|
+
// a developer's local main may be ahead of origin/main.
|
|
145
|
+
candidates.push(`origin/${normalizedBaseBranch}`);
|
|
146
|
+
candidates.push(normalizedBaseBranch);
|
|
147
|
+
}
|
|
148
|
+
else {
|
|
149
|
+
// Try the raw value first — handles commit SHAs, tags, and explicit local refs.
|
|
150
|
+
candidates.push(normalizedBaseBranch);
|
|
151
|
+
if (!isRemoteQualified) {
|
|
152
|
+
candidates.push(`origin/${normalizedBaseBranch}`);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
71
155
|
}
|
|
72
156
|
// Auto-detect remote default branch
|
|
73
157
|
try {
|
|
@@ -89,12 +173,13 @@ export async function computeBranchDiff(repositoryPath, providedBaseBranch) {
|
|
|
89
173
|
// Final fallback
|
|
90
174
|
candidates.push("HEAD~1");
|
|
91
175
|
for (const ref of candidates) {
|
|
92
|
-
const result = await tryDiff(git, ref);
|
|
176
|
+
const result = await tryDiff(git, ref, includeUncommitted);
|
|
93
177
|
if (result) {
|
|
94
178
|
logger.info("Branch diff computed", {
|
|
95
179
|
currentBranch,
|
|
96
180
|
baseBranch: ref,
|
|
97
181
|
changedFiles: result.changedFiles.length,
|
|
182
|
+
includeUncommitted,
|
|
98
183
|
});
|
|
99
184
|
return { currentBranch, baseBranch: ref, ...result };
|
|
100
185
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { jest } from "@jest/globals";
|
|
2
|
+
import { execFileSync } from "child_process";
|
|
3
|
+
import fs from "fs";
|
|
4
|
+
import os from "os";
|
|
5
|
+
import path from "path";
|
|
6
|
+
import { computeBranchDiff, normalizeProvidedBaseRef } from "./branchDiff.js";
|
|
7
|
+
jest.setTimeout(15000);
|
|
8
|
+
function git(cwd, args) {
|
|
9
|
+
execFileSync("git", args, { cwd, stdio: "ignore" });
|
|
10
|
+
}
|
|
11
|
+
describe("computeBranchDiff", () => {
|
|
12
|
+
let tmpDir;
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "skyramp-branch-diff-"));
|
|
15
|
+
git(tmpDir, ["init"]);
|
|
16
|
+
git(tmpDir, ["branch", "-m", "main"]);
|
|
17
|
+
fs.writeFileSync(path.join(tmpDir, "README.md"), "initial\n");
|
|
18
|
+
git(tmpDir, ["add", "README.md"]);
|
|
19
|
+
git(tmpDir, [
|
|
20
|
+
"-c",
|
|
21
|
+
"user.name=Test",
|
|
22
|
+
"-c",
|
|
23
|
+
"user.email=test@example.com",
|
|
24
|
+
"commit",
|
|
25
|
+
"-m",
|
|
26
|
+
"initial",
|
|
27
|
+
]);
|
|
28
|
+
});
|
|
29
|
+
afterEach(() => {
|
|
30
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
31
|
+
});
|
|
32
|
+
it("classifies untracked files as both changed and new when including uncommitted changes", async () => {
|
|
33
|
+
fs.mkdirSync(path.join(tmpDir, "src"));
|
|
34
|
+
fs.writeFileSync(path.join(tmpDir, "src", "newEndpoint.ts"), "export const value = 1;\n");
|
|
35
|
+
const diff = await computeBranchDiff(tmpDir, "HEAD", true);
|
|
36
|
+
expect(diff.changedFiles).toContain("src/newEndpoint.ts");
|
|
37
|
+
expect(diff.newFiles).toContain("src/newEndpoint.ts");
|
|
38
|
+
});
|
|
39
|
+
it("prefers origin branch over local branch for uncommitted local-dev diffs", async () => {
|
|
40
|
+
const remoteDir = fs.mkdtempSync(path.join(os.tmpdir(), "skyramp-branch-diff-remote-"));
|
|
41
|
+
try {
|
|
42
|
+
git(remoteDir, ["init", "--bare"]);
|
|
43
|
+
git(tmpDir, ["remote", "add", "origin", remoteDir]);
|
|
44
|
+
git(tmpDir, ["push", "-u", "origin", "main"]);
|
|
45
|
+
fs.writeFileSync(path.join(tmpDir, "README.md"), "local main ahead\n");
|
|
46
|
+
git(tmpDir, ["add", "README.md"]);
|
|
47
|
+
git(tmpDir, [
|
|
48
|
+
"-c",
|
|
49
|
+
"user.name=Test",
|
|
50
|
+
"-c",
|
|
51
|
+
"user.email=test@example.com",
|
|
52
|
+
"commit",
|
|
53
|
+
"-m",
|
|
54
|
+
"local main ahead",
|
|
55
|
+
]);
|
|
56
|
+
fs.writeFileSync(path.join(tmpDir, "local.txt"), "uncommitted\n");
|
|
57
|
+
const diff = await computeBranchDiff(tmpDir, "main", true);
|
|
58
|
+
expect(diff.baseBranch).toBe("origin/main");
|
|
59
|
+
expect(diff.changedFiles.join("\n")).not.toMatch(/^origin\//m);
|
|
60
|
+
expect(diff.changedFiles).toContain("README.md");
|
|
61
|
+
expect(diff.changedFiles).toContain("local.txt");
|
|
62
|
+
}
|
|
63
|
+
finally {
|
|
64
|
+
fs.rmSync(remoteDir, { recursive: true, force: true });
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
it("uses SHA-like base refs directly when including uncommitted changes", async () => {
|
|
68
|
+
const remoteDir = fs.mkdtempSync(path.join(os.tmpdir(), "skyramp-branch-diff-remote-"));
|
|
69
|
+
try {
|
|
70
|
+
git(remoteDir, ["init", "--bare"]);
|
|
71
|
+
git(tmpDir, ["remote", "add", "origin", remoteDir]);
|
|
72
|
+
git(tmpDir, ["push", "-u", "origin", "main"]);
|
|
73
|
+
const baseSha = execFileSync("git", ["rev-parse", "--short=7", "HEAD"], {
|
|
74
|
+
cwd: tmpDir,
|
|
75
|
+
encoding: "utf8",
|
|
76
|
+
}).trim();
|
|
77
|
+
fs.writeFileSync(path.join(tmpDir, "README.md"), "local main ahead\n");
|
|
78
|
+
git(tmpDir, ["add", "README.md"]);
|
|
79
|
+
git(tmpDir, [
|
|
80
|
+
"-c",
|
|
81
|
+
"user.name=Test",
|
|
82
|
+
"-c",
|
|
83
|
+
"user.email=test@example.com",
|
|
84
|
+
"commit",
|
|
85
|
+
"-m",
|
|
86
|
+
"local main ahead",
|
|
87
|
+
]);
|
|
88
|
+
git(tmpDir, ["branch", baseSha, "HEAD"]);
|
|
89
|
+
git(tmpDir, ["push", "origin", baseSha]);
|
|
90
|
+
fs.writeFileSync(path.join(tmpDir, "local.txt"), "uncommitted\n");
|
|
91
|
+
const diff = await computeBranchDiff(tmpDir, baseSha, true);
|
|
92
|
+
expect(diff.baseBranch).toBe(baseSha);
|
|
93
|
+
expect(diff.baseBranch).not.toMatch(/^origin\//);
|
|
94
|
+
expect(diff.changedFiles).toContain("local.txt");
|
|
95
|
+
}
|
|
96
|
+
finally {
|
|
97
|
+
fs.rmSync(remoteDir, { recursive: true, force: true });
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
it("trims provided base refs before using them", () => {
|
|
101
|
+
expect(normalizeProvidedBaseRef(" main ")).toBe("main");
|
|
102
|
+
});
|
|
103
|
+
it("rejects option-like provided base refs", () => {
|
|
104
|
+
expect(() => normalizeProvidedBaseRef("--output=/tmp/evil")).toThrow("refs must not start with '-'");
|
|
105
|
+
});
|
|
106
|
+
it("rejects provided base refs containing internal whitespace", () => {
|
|
107
|
+
expect(() => normalizeProvidedBaseRef("main --stat")).toThrow("refs must not contain whitespace");
|
|
108
|
+
});
|
|
109
|
+
});
|
|
@@ -35,3 +35,19 @@ export declare function isContractConsumerModeEnabled(): boolean;
|
|
|
35
35
|
* (SKYRAMP_FEATURE_TESTBOT=1).
|
|
36
36
|
*/
|
|
37
37
|
export declare function isTestbotEnabled(): boolean;
|
|
38
|
+
/**
|
|
39
|
+
* Returns true when the local-dev workflow is enabled
|
|
40
|
+
* (SKYRAMP_FEATURE_LOCAL_DEV=1).
|
|
41
|
+
*
|
|
42
|
+
* The local-dev workflow registers the `skyramp_local_dev` prompt and
|
|
43
|
+
* one-click routing, which diffs from a base commit against the current
|
|
44
|
+
* working state, generates mock configurations and backend tests, executes
|
|
45
|
+
* them locally, and reports results — without requiring a pull request or
|
|
46
|
+
* CI environment.
|
|
47
|
+
*/
|
|
48
|
+
export declare function isLocalDevEnabled(): boolean;
|
|
49
|
+
/**
|
|
50
|
+
* Returns true when general one-click workflows are enabled
|
|
51
|
+
* (SKYRAMP_FEATURE_ONE_CLICK=1).
|
|
52
|
+
*/
|
|
53
|
+
export declare function isOneClickEnabled(): boolean;
|
|
@@ -39,3 +39,23 @@ export function isContractConsumerModeEnabled() {
|
|
|
39
39
|
export function isTestbotEnabled() {
|
|
40
40
|
return process.env.SKYRAMP_FEATURE_TESTBOT === "1";
|
|
41
41
|
}
|
|
42
|
+
/**
|
|
43
|
+
* Returns true when the local-dev workflow is enabled
|
|
44
|
+
* (SKYRAMP_FEATURE_LOCAL_DEV=1).
|
|
45
|
+
*
|
|
46
|
+
* The local-dev workflow registers the `skyramp_local_dev` prompt and
|
|
47
|
+
* one-click routing, which diffs from a base commit against the current
|
|
48
|
+
* working state, generates mock configurations and backend tests, executes
|
|
49
|
+
* them locally, and reports results — without requiring a pull request or
|
|
50
|
+
* CI environment.
|
|
51
|
+
*/
|
|
52
|
+
export function isLocalDevEnabled() {
|
|
53
|
+
return process.env.SKYRAMP_FEATURE_LOCAL_DEV === "1";
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Returns true when general one-click workflows are enabled
|
|
57
|
+
* (SKYRAMP_FEATURE_ONE_CLICK=1).
|
|
58
|
+
*/
|
|
59
|
+
export function isOneClickEnabled() {
|
|
60
|
+
return process.env.SKYRAMP_FEATURE_ONE_CLICK === "1";
|
|
61
|
+
}
|
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
import { resolveServiceDetailsRef } from "./utils.js";
|
|
2
|
-
import { isTestbotEnabled } from "./featureFlags.js";
|
|
2
|
+
import { isContractConsumerModeEnabled, isLocalDevEnabled, isTestbotEnabled } from "./featureFlags.js";
|
|
3
3
|
// Capture and restore the original env var so these tests don't pollute other
|
|
4
4
|
// test suites or a developer's shell environment where the var may already be set.
|
|
5
5
|
let originalTestbotEnv;
|
|
6
|
-
|
|
6
|
+
let originalContractConsumerEnv;
|
|
7
|
+
let originalLocalDevEnv;
|
|
8
|
+
beforeAll(() => {
|
|
9
|
+
originalTestbotEnv = process.env.SKYRAMP_FEATURE_TESTBOT;
|
|
10
|
+
originalContractConsumerEnv = process.env.SKYRAMP_FEATURE_CONTRACT_CONSUMER_MODE;
|
|
11
|
+
originalLocalDevEnv = process.env.SKYRAMP_FEATURE_LOCAL_DEV;
|
|
12
|
+
});
|
|
7
13
|
afterAll(() => {
|
|
8
14
|
if (originalTestbotEnv === undefined) {
|
|
9
15
|
delete process.env.SKYRAMP_FEATURE_TESTBOT;
|
|
@@ -11,6 +17,18 @@ afterAll(() => {
|
|
|
11
17
|
else {
|
|
12
18
|
process.env.SKYRAMP_FEATURE_TESTBOT = originalTestbotEnv;
|
|
13
19
|
}
|
|
20
|
+
if (originalContractConsumerEnv === undefined) {
|
|
21
|
+
delete process.env.SKYRAMP_FEATURE_CONTRACT_CONSUMER_MODE;
|
|
22
|
+
}
|
|
23
|
+
else {
|
|
24
|
+
process.env.SKYRAMP_FEATURE_CONTRACT_CONSUMER_MODE = originalContractConsumerEnv;
|
|
25
|
+
}
|
|
26
|
+
if (originalLocalDevEnv === undefined) {
|
|
27
|
+
delete process.env.SKYRAMP_FEATURE_LOCAL_DEV;
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
process.env.SKYRAMP_FEATURE_LOCAL_DEV = originalLocalDevEnv;
|
|
31
|
+
}
|
|
14
32
|
});
|
|
15
33
|
describe("isTestbotEnabled", () => {
|
|
16
34
|
afterEach(() => {
|
|
@@ -32,6 +50,43 @@ describe("isTestbotEnabled", () => {
|
|
|
32
50
|
expect(isTestbotEnabled()).toBe(false);
|
|
33
51
|
});
|
|
34
52
|
});
|
|
53
|
+
describe("isContractConsumerModeEnabled", () => {
|
|
54
|
+
afterEach(() => {
|
|
55
|
+
delete process.env.SKYRAMP_FEATURE_CONTRACT_CONSUMER_MODE;
|
|
56
|
+
delete process.env.SKYRAMP_FEATURE_LOCAL_DEV;
|
|
57
|
+
});
|
|
58
|
+
it("defaults to false when neither flag is set", () => {
|
|
59
|
+
expect(isContractConsumerModeEnabled()).toBe(false);
|
|
60
|
+
});
|
|
61
|
+
it('returns true when SKYRAMP_FEATURE_CONTRACT_CONSUMER_MODE is "1"', () => {
|
|
62
|
+
process.env.SKYRAMP_FEATURE_CONTRACT_CONSUMER_MODE = "1";
|
|
63
|
+
expect(isContractConsumerModeEnabled()).toBe(true);
|
|
64
|
+
});
|
|
65
|
+
it('returns false when SKYRAMP_FEATURE_LOCAL_DEV is "1" alone (local-dev no longer implies consumer mode)', () => {
|
|
66
|
+
process.env.SKYRAMP_FEATURE_LOCAL_DEV = "1";
|
|
67
|
+
expect(isContractConsumerModeEnabled()).toBe(false);
|
|
68
|
+
});
|
|
69
|
+
it('returns false when SKYRAMP_FEATURE_CONTRACT_CONSUMER_MODE is "0"', () => {
|
|
70
|
+
process.env.SKYRAMP_FEATURE_CONTRACT_CONSUMER_MODE = "0";
|
|
71
|
+
expect(isContractConsumerModeEnabled()).toBe(false);
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
describe("isLocalDevEnabled", () => {
|
|
75
|
+
afterEach(() => {
|
|
76
|
+
delete process.env.SKYRAMP_FEATURE_LOCAL_DEV;
|
|
77
|
+
});
|
|
78
|
+
it('returns true when SKYRAMP_FEATURE_LOCAL_DEV is "1"', () => {
|
|
79
|
+
process.env.SKYRAMP_FEATURE_LOCAL_DEV = "1";
|
|
80
|
+
expect(isLocalDevEnabled()).toBe(true);
|
|
81
|
+
});
|
|
82
|
+
it("returns false when SKYRAMP_FEATURE_LOCAL_DEV is unset", () => {
|
|
83
|
+
expect(isLocalDevEnabled()).toBe(false);
|
|
84
|
+
});
|
|
85
|
+
it('returns false when SKYRAMP_FEATURE_LOCAL_DEV is "0"', () => {
|
|
86
|
+
process.env.SKYRAMP_FEATURE_LOCAL_DEV = "0";
|
|
87
|
+
expect(isLocalDevEnabled()).toBe(false);
|
|
88
|
+
});
|
|
89
|
+
});
|
|
35
90
|
describe("resolveServiceDetailsRef", () => {
|
|
36
91
|
afterEach(() => {
|
|
37
92
|
delete process.env.SKYRAMP_FEATURE_TESTBOT;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function getGrpcMockResponseValidationError(responseData?: string): string | null;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
const GRPC_STATUS_ERROR_NAMES = new Set([
|
|
2
|
+
"CANCELLED",
|
|
3
|
+
"UNKNOWN",
|
|
4
|
+
"INVALID_ARGUMENT",
|
|
5
|
+
"DEADLINE_EXCEEDED",
|
|
6
|
+
"NOT_FOUND",
|
|
7
|
+
"ALREADY_EXISTS",
|
|
8
|
+
"PERMISSION_DENIED",
|
|
9
|
+
"RESOURCE_EXHAUSTED",
|
|
10
|
+
"FAILED_PRECONDITION",
|
|
11
|
+
"ABORTED",
|
|
12
|
+
"OUT_OF_RANGE",
|
|
13
|
+
"UNIMPLEMENTED",
|
|
14
|
+
"INTERNAL",
|
|
15
|
+
"UNAVAILABLE",
|
|
16
|
+
"DATA_LOSS",
|
|
17
|
+
"UNAUTHENTICATED",
|
|
18
|
+
]);
|
|
19
|
+
export function getGrpcMockResponseValidationError(responseData) {
|
|
20
|
+
if (!responseData || responseData.trim().startsWith("@"))
|
|
21
|
+
return null;
|
|
22
|
+
let parsed;
|
|
23
|
+
try {
|
|
24
|
+
parsed = JSON.parse(responseData);
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
const record = parsed;
|
|
33
|
+
const candidate = record.error;
|
|
34
|
+
if (typeof candidate !== "string")
|
|
35
|
+
return null;
|
|
36
|
+
const normalized = normalizeGrpcStatusName(candidate);
|
|
37
|
+
if (!GRPC_STATUS_ERROR_NAMES.has(normalized))
|
|
38
|
+
return null;
|
|
39
|
+
return `gRPC mock responseData cannot simulate gRPC status errors such as "${normalized}". Use grpcStatusCode/grpcStatusMessage for gRPC status errors, generate a successful protobuf-shaped response, use a real failing service, or record this scenario as blocked`;
|
|
40
|
+
}
|
|
41
|
+
function normalizeGrpcStatusName(value) {
|
|
42
|
+
return value
|
|
43
|
+
.replace(/^GRPC::Status(?:::|\.)?/i, "")
|
|
44
|
+
.replace(/^GRPC::/i, "")
|
|
45
|
+
.replace(/([a-z0-9])([A-Z])/g, "$1_$2")
|
|
46
|
+
.replace(/[\s.:-]+/g, "_")
|
|
47
|
+
.replace(/^_+|_+$/g, "")
|
|
48
|
+
.toUpperCase();
|
|
49
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { getGrpcMockResponseValidationError } from "./grpcMockValidation.js";
|
|
2
|
+
describe("getGrpcMockResponseValidationError", () => {
|
|
3
|
+
it.each([
|
|
4
|
+
["UNAVAILABLE", "UNAVAILABLE"],
|
|
5
|
+
["GRPC::Unavailable", "UNAVAILABLE"],
|
|
6
|
+
["GRPC::Status::UNAVAILABLE", "UNAVAILABLE"],
|
|
7
|
+
["GRPC::Status.UNAVAILABLE", "UNAVAILABLE"],
|
|
8
|
+
["DeadlineExceeded", "DEADLINE_EXCEEDED"],
|
|
9
|
+
])("rejects gRPC status error spelling %s", (input, normalized) => {
|
|
10
|
+
expect(getGrpcMockResponseValidationError(JSON.stringify({ error: input }))).toBe(`gRPC mock responseData cannot simulate gRPC status errors such as "${normalized}". Use grpcStatusCode/grpcStatusMessage for gRPC status errors, generate a successful protobuf-shaped response, use a real failing service, or record this scenario as blocked`);
|
|
11
|
+
});
|
|
12
|
+
it("allows successful protobuf-shaped response bodies", () => {
|
|
13
|
+
expect(getGrpcMockResponseValidationError(JSON.stringify({ tax_exempt: true, features: ["billing"] }))).toBeNull();
|
|
14
|
+
});
|
|
15
|
+
it("returns null when responseData is undefined or empty", () => {
|
|
16
|
+
expect(getGrpcMockResponseValidationError(undefined)).toBeNull();
|
|
17
|
+
expect(getGrpcMockResponseValidationError("")).toBeNull();
|
|
18
|
+
});
|
|
19
|
+
it("returns null when responseData is a file reference", () => {
|
|
20
|
+
expect(getGrpcMockResponseValidationError("@/repo/fixtures/response.json")).toBeNull();
|
|
21
|
+
});
|
|
22
|
+
it("returns null when responseData is a JSON array", () => {
|
|
23
|
+
expect(getGrpcMockResponseValidationError("[1, 2, 3]")).toBeNull();
|
|
24
|
+
});
|
|
25
|
+
it("returns null when responseData is JSON null", () => {
|
|
26
|
+
expect(getGrpcMockResponseValidationError("null")).toBeNull();
|
|
27
|
+
});
|
|
28
|
+
it("returns null when error field is a non-string value", () => {
|
|
29
|
+
expect(getGrpcMockResponseValidationError(JSON.stringify({ error: 14 }))).toBeNull();
|
|
30
|
+
expect(getGrpcMockResponseValidationError(JSON.stringify({ error: null }))).toBeNull();
|
|
31
|
+
});
|
|
32
|
+
it.each(["status", "code", "statusCode"])("allows bare application status fields in %s field", (field) => {
|
|
33
|
+
expect(getGrpcMockResponseValidationError(JSON.stringify({ [field]: "NOT_FOUND" }))).toBeNull();
|
|
34
|
+
});
|
|
35
|
+
it("allows protobuf-shaped responses with application status fields", () => {
|
|
36
|
+
expect(getGrpcMockResponseValidationError(JSON.stringify({ status: "INTERNAL", user_id: 123 }))).toBeNull();
|
|
37
|
+
});
|
|
38
|
+
it("returns null when error field is a non-status-code string", () => {
|
|
39
|
+
expect(getGrpcMockResponseValidationError(JSON.stringify({ error: "user not found" }))).toBeNull();
|
|
40
|
+
});
|
|
41
|
+
});
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { HttpMethod } from "../types/TestTypes.js";
|
|
2
|
+
export declare const REST_MOCK_HTTP_METHODS: HttpMethod[];
|
|
3
|
+
export declare function normalizeRestMockMethod(method?: string): string;
|
|
4
|
+
export declare function getRestMockMethodValidationError(method?: string): string | null;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { HttpMethod } from "../types/TestTypes.js";
|
|
2
|
+
export const REST_MOCK_HTTP_METHODS = Object.values(HttpMethod);
|
|
3
|
+
export function normalizeRestMockMethod(method) {
|
|
4
|
+
return method ? method.trim().toUpperCase() : "";
|
|
5
|
+
}
|
|
6
|
+
export function getRestMockMethodValidationError(method) {
|
|
7
|
+
if (!method)
|
|
8
|
+
return null;
|
|
9
|
+
const normalized = normalizeRestMockMethod(method);
|
|
10
|
+
if (!normalized)
|
|
11
|
+
return null; // whitespace-only → treated as empty (valid)
|
|
12
|
+
if (REST_MOCK_HTTP_METHODS.includes(normalized))
|
|
13
|
+
return null;
|
|
14
|
+
return `REST mock method must be one of ${REST_MOCK_HTTP_METHODS.join(", ")} or empty string (got "${method}")`;
|
|
15
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
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
|
+
/** Narrow view of a mock spec used only for protocol compatibility checking. */
|
|
10
|
+
export interface MockCompatibilitySpec {
|
|
11
|
+
protocol: MockProtocol;
|
|
12
|
+
endpointURL: string;
|
|
13
|
+
method?: string;
|
|
14
|
+
grpcServiceName?: string;
|
|
15
|
+
kafkaTopic?: string;
|
|
16
|
+
}
|
|
17
|
+
export interface CompatibilityWarning {
|
|
18
|
+
code: string;
|
|
19
|
+
message: string;
|
|
20
|
+
suggestion: string;
|
|
21
|
+
affectedMocks: string[];
|
|
22
|
+
/** When true, the affected mocks cannot intercept traffic and must not be generated or deployed. */
|
|
23
|
+
blocking?: boolean;
|
|
24
|
+
}
|
|
25
|
+
export interface CompatibilityResult {
|
|
26
|
+
compatible: boolean;
|
|
27
|
+
/** True when at least one warning is blocking — those mocks must be skipped entirely. */
|
|
28
|
+
hasBlockers: boolean;
|
|
29
|
+
warnings: CompatibilityWarning[];
|
|
30
|
+
}
|
|
31
|
+
export interface MockCompatibilityOptions {
|
|
32
|
+
/** Enable Docker local-dev checks that require mocks to intercept service DNS aliases. */
|
|
33
|
+
dockerLocalDev?: boolean;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Validates a set of mock specs for known protocol compatibility issues.
|
|
37
|
+
*
|
|
38
|
+
* Rules:
|
|
39
|
+
* - Any gRPC mock present → GRPC_PERSISTENT_CONN warning (DNS hijacking skips persistent conns)
|
|
40
|
+
* - Docker local-dev REST mock using loopback/host.docker.internal → REST_LOOPBACK_URL blocking warning
|
|
41
|
+
*
|
|
42
|
+
* REST mocks work correctly in Docker local-dev mode only when the mock URL uses
|
|
43
|
+
* the real Docker service hostname (e.g. "billing" from "http://billing:5001").
|
|
44
|
+
* The worker adds that hostname as a Docker network alias so DNS resolves to the mock server.
|
|
45
|
+
*
|
|
46
|
+
* `compatible` is false when any warning is blocking and the mock cannot intercept traffic.
|
|
47
|
+
* `hasBlockers` is true when at least one warning is blocking (mock must not be generated/deployed).
|
|
48
|
+
*/
|
|
49
|
+
export declare function validateMockCompatibility(mocks: MockCompatibilitySpec[], options?: MockCompatibilityOptions): CompatibilityResult;
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|