@skyramp/skyramp 1.3.13 → 1.3.14

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skyramp/skyramp",
3
- "version": "1.3.13",
3
+ "version": "1.3.14",
4
4
  "description": "module for leveraging skyramp cli functionality",
5
5
  "scripts": {
6
6
  "lint": "eslint 'src/**/*.js' 'src/**/*.ts' --fix",
@@ -4,6 +4,18 @@ const path = require('path');
4
4
  const crypto = require('crypto');
5
5
  const { S3Client, GetObjectCommand } = require("@aws-sdk/client-s3");
6
6
 
7
+ const HEAD_TIMEOUT_MS = 30_000; // 30s for HEAD/ETag check
8
+ const DOWNLOAD_TIMEOUT_MS = 300_000; // 5min for binary download
9
+ const SCRIPT_TIMEOUT_MS = 600_000; // 10min global script timeout
10
+
11
+ // Global script timeout to prevent hanging indefinitely
12
+ const scriptTimer = setTimeout(() => {
13
+ log('error', `Script timed out after ${SCRIPT_TIMEOUT_MS / 1000}s. This is likely a network connectivity issue.`);
14
+ log('error', `Please check your network connection and retry by running: npm install @skyramp/skyramp`);
15
+ process.exit(1);
16
+ }, SCRIPT_TIMEOUT_MS);
17
+ scriptTimer.unref();
18
+
7
19
  function log(level, message) {
8
20
  const timestamp = new Date().toISOString();
9
21
  console.log(`[${timestamp}] [${level.toUpperCase()}] ${message}`);
@@ -79,13 +91,22 @@ async function calculateMD5(filePath) {
79
91
 
80
92
  async function getETag(url) {
81
93
  return new Promise((resolve, reject) => {
82
- https.get(url, { method: 'HEAD' }, res => {
94
+ const req = https.get(url, { method: 'HEAD', timeout: HEAD_TIMEOUT_MS }, res => {
83
95
  if (res.statusCode !== 200) {
96
+ res.resume(); // Drain response to free the socket
84
97
  reject(new Error(`HTTP ${res.statusCode}: ${url}`));
85
98
  return;
86
99
  }
100
+ res.on('error', (err) => {
101
+ reject(new Error(`Response stream error during HEAD request: ${err.message}`));
102
+ });
87
103
  resolve(res.headers['etag']?.replace(/"/g, '')); // Remove quotes from ETag
88
- }).on('error', (err) => {
104
+ });
105
+ req.on('timeout', () => {
106
+ req.destroy();
107
+ reject(new Error(`HEAD request timed out after ${HEAD_TIMEOUT_MS / 1000}s: ${url}`));
108
+ });
109
+ req.on('error', (err) => {
89
110
  log('error', `Error during HEAD request to ${url}: ${err.message}`);
90
111
  reject(err);
91
112
  });
@@ -119,22 +140,61 @@ async function download(url, dest, options = {}) {
119
140
  });
120
141
  } else {
121
142
  // Public URL download using https
122
- https.get(url, res => {
143
+ let rejected = false;
144
+ const fail = (err) => {
145
+ if (rejected) return;
146
+ rejected = true;
147
+ // Clean up partial file
148
+ try { fs.unlinkSync(dest); } catch (_) {}
149
+ reject(err);
150
+ };
151
+
152
+ const req = https.get(url, { timeout: DOWNLOAD_TIMEOUT_MS }, res => {
123
153
  if (res.statusCode !== 200) {
154
+ res.resume(); // Drain response to free the socket
124
155
  const error = new Error(`HTTP ${res.statusCode}: ${url}`);
125
156
  log('error', `Failed to download ${url}: ${error.message}`);
126
- reject(error);
157
+ fail(error);
127
158
  return;
128
159
  }
129
160
  const file = fs.createWriteStream(dest);
161
+ let downloaded = 0;
162
+ const contentLength = parseInt(res.headers['content-length'], 10);
163
+ let lastLogTime = Date.now();
164
+
165
+ res.on('data', (chunk) => {
166
+ downloaded += chunk.length;
167
+ const now = Date.now();
168
+ if (now - lastLogTime > 10_000) { // Log progress every 10s
169
+ const pct = contentLength ? ` (${Math.round(downloaded / contentLength * 100)}%)` : '';
170
+ log('info', `Downloading ${path.basename(dest)}: ${(downloaded / 1024 / 1024).toFixed(1)} MB${pct}`);
171
+ lastLogTime = now;
172
+ }
173
+ });
174
+
175
+ res.on('error', (err) => {
176
+ file.destroy();
177
+ fail(new Error(`Response stream error: ${err.message}`));
178
+ });
179
+
180
+ file.on('error', (err) => {
181
+ res.destroy();
182
+ fail(new Error(`File write error: ${err.message}`));
183
+ });
184
+
130
185
  res.pipe(file);
131
186
  file.on('finish', () => {
132
187
  log('info', `Successfully downloaded ${url} to ${dest}`);
133
188
  file.close(resolve);
134
189
  });
135
- }).on('error', (err) => {
190
+ });
191
+ req.on('timeout', () => {
192
+ req.destroy();
193
+ fail(new Error(`Download timed out after ${DOWNLOAD_TIMEOUT_MS / 1000}s: ${url}`));
194
+ });
195
+ req.on('error', (err) => {
136
196
  log('error', `Error during download from ${url}: ${err.message}`);
137
- reject(err);
197
+ fail(err);
138
198
  });
139
199
  }
140
200
  });
@@ -175,6 +235,23 @@ async function download(url, dest, options = {}) {
175
235
  }
176
236
 
177
237
  await download(url, file.dest, options);
238
+
239
+ // Verify downloaded file integrity via ETag/MD5
240
+ if (!S3_PRIVATE) {
241
+ try {
242
+ const remoteETag = await getETag(url);
243
+ const localHash = await calculateMD5(file.dest);
244
+ if (remoteETag && remoteETag !== localHash) {
245
+ log('error', `Hash mismatch for ${file.name}: expected ${remoteETag}, got ${localHash}`);
246
+ try { fs.unlinkSync(file.dest); } catch (_) {}
247
+ throw new Error(`Integrity check failed for ${file.name}. Please check your network connection and retry.`);
248
+ }
249
+ } catch (hashErr) {
250
+ if (hashErr.message.startsWith('Integrity check failed')) throw hashErr;
251
+ log('warn', `Could not verify hash for ${file.name}: ${hashErr.message}`);
252
+ }
253
+ }
254
+
178
255
  log('info', `✅ Saved ${file.name} to ${file.dest}`);
179
256
  } catch (e) {
180
257
  log('error', `❌ Failed to process ${file.name}: ${e.message}`);
@@ -108,8 +108,9 @@ const generateRestTestWrapper = lib.func('generateRestTestWrapper', 'string', [
108
108
  'bool', // providerMode
109
109
  'bool', // consumerMode
110
110
  'string', // providerOutput (contract test)
111
- 'string', // consumerOutput (contract test)
112
- 'bool' // skipProvisionParents
111
+ 'string', // consumerOutput (contract test)
112
+ 'bool', // skipProvisionParents
113
+ 'int' // mockPort
113
114
  ]);
114
115
  const generateRestMockWrapper = lib.func('generateRestMockWrapper', 'string', [
115
116
  'string', // uri
@@ -124,17 +125,17 @@ const generateRestMockWrapper = lib.func('generateRestMockWrapper', 'string', [
124
125
  'string', // k8sNamespace
125
126
  'string', // k8sConfig
126
127
  'string', // k8sContext
128
+ 'string', // requestData
127
129
  'string', // responseData
128
130
  'string', // responseStatusCode
129
131
  'bool', // force
130
132
  'bool', // deployDashboard
131
133
  'bool', // requestAware
132
134
  'string', // formParams
133
- 'string', // pathParams
134
- 'string', // queryParams
135
135
  'string', // apiSchema
136
136
  'string', // traceFilePath
137
- 'string' // entryPoint
137
+ 'string', // entryPoint
138
+ 'int' // mockPort
138
139
  ]);
139
140
  const traceCollectWrapper = lib.func('traceCollectWrapper', 'string', ['string', 'string', 'bool', 'string', 'string']);
140
141
  const analyzeOpenapiWrapper = lib.func('analyzeOpenapiWrapper', 'string', ['string', 'string']);
@@ -948,11 +949,12 @@ class SkyrampClient {
948
949
  parentRequestData || "",
949
950
  parentStatusCode || "",
950
951
  options.requestAware || false,
951
- options.providerMode || true,
952
+ options.providerMode || false,
952
953
  options.consumerMode || false,
953
954
  options.providerOutput || "",
954
955
  options.consumerOutput || "",
955
- options.skipProvisionParents || true,
956
+ options.skipProvisionParents || false,
957
+ options.mockPort || 0,
956
958
  (err, res) => {
957
959
  if (err) {
958
960
  reject(err);
@@ -979,17 +981,17 @@ class SkyrampClient {
979
981
  options.k8sNamespace || "",
980
982
  options.k8sConfig || "",
981
983
  options.k8sContext || "",
984
+ options.requestData || "",
982
985
  options.responseData || "",
983
986
  options.responseStatusCode || "",
984
987
  options.force || false,
985
988
  options.deployDashboard || false,
986
989
  options.requestAware || false,
987
990
  options.formParams || "",
988
- options.pathParams || "",
989
- options.queryParams || "",
990
991
  JSON.stringify(options.apiSchema || []),
991
992
  options.traceFilePath || "",
992
993
  options.entrypoint || "",
994
+ options.mockPort || 0,
993
995
  (err, res) => {
994
996
  if (err) {
995
997
  reject(err);
@@ -1175,6 +1175,11 @@ class SkyrampPlaywrightPage {
1175
1175
  return newLocator
1176
1176
  }
1177
1177
 
1178
+ frameLocator(selector) {
1179
+ const originalFrameLocator = this._page.frameLocator(selector);
1180
+ return new SkyrampPlaywrightFrameLocator(this, originalFrameLocator);
1181
+ }
1182
+
1178
1183
  locator(selector, options) {
1179
1184
  const originalLocator = this._page.locator(selector, options);
1180
1185
  return this.newSkyrampPlaywrightLocator(originalLocator, selector, options);
@@ -1434,5 +1439,6 @@ function expect(obj, testInfo) {
1434
1439
 
1435
1440
  module.exports = {
1436
1441
  newSkyrampPlaywrightPage,
1442
+ SkyrampPlaywrightPage,
1437
1443
  expect,
1438
1444
  };
@@ -26,7 +26,7 @@ export interface Service {
26
26
  serviceName: string;
27
27
  language?: "python" | "typescript" | "javascript" | "java";
28
28
  framework?: "playwright" | "pytest" | "robot" | "junit";
29
- outputDir: string;
29
+ testDirectory?: string;
30
30
  api?: ServiceApi;
31
31
  runtimeDetails?: ServiceRuntimeDetails;
32
32
  }
package/src/workspace.js CHANGED
@@ -26,7 +26,7 @@ const serviceSchema = z.object({
26
26
  framework: z
27
27
  .enum(['playwright', 'pytest', 'robot', 'junit'])
28
28
  .optional(),
29
- outputDir: z.string().optional(),
29
+ testDirectory: z.string().optional(),
30
30
  api: z
31
31
  .object({
32
32
  schemaPath: z.string().optional(),