@appliqation/automation-sdk 2.4.0 → 2.5.1

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.
@@ -4,6 +4,7 @@ const RunMatrixService = require('./services/RunMatrixService');
4
4
  const ResultService = require('./services/ResultService');
5
5
  const TaggingService = require('./services/TaggingService');
6
6
  const OrphanTestService = require('./services/OrphanTestService');
7
+ const ProjectInfoService = require('./services/ProjectInfoService');
7
8
  const UuidValidator = require('./utils/UuidValidator');
8
9
  const PayloadBuilder = require('./utils/PayloadBuilder');
9
10
  const logger = require('./utils/logger');
@@ -13,102 +14,82 @@ const { DEFAULT_APPLIQATION_BASE_URL } = require('./constants');
13
14
  * Main Appliqation Automation SDK Client
14
15
  *
15
16
  * @example
16
- * // Initialize with API key (recommended for automation)
17
+ * // Minimal config project auto-discovered from API key
17
18
  * const client = new AppliqationClient({
18
- * baseUrl: 'https://your-instance.appliqation.com',
19
- * apiKey: 'appq_live_xxxxxxxxxxxxx',
20
- * projectKey: 'your-project-key'
21
- * });
22
- *
23
- * // Create run matrix
24
- * const run = await client.createRun({
25
- * scenarioId: 123,
26
- * environment: 'Production',
27
- * browsers: ['Chrome', 'Firefox'],
28
- * device: 'Desktop',
29
- * os: 'Windows 11'
30
- * });
31
- *
32
- * // Submit test results
33
- * await client.submitResult(run.runId, {
34
- * uuid: '124-d1f9559c-b978-43cc-9c76-fd539c717cb4',
35
- * status: 'passed',
36
- * browser: 'Chrome'
19
+ * apiKey: process.env.APPLIQATION_API_KEY
37
20
  * });
21
+ * await client.initialize(); // auto-discovers project
38
22
  */
39
23
  class AppliqationClient {
24
+
40
25
  /**
41
- * Create Appliqation client
42
- * @param {Object} config - Client configuration
43
- * @param {string} config.baseUrl - Appliqation instance URL
44
- * @param {string} config.apiKey - API key for authentication (recommended)
45
- * @param {string} config.projectKey - Project key (base64 encoded or plain)
46
- * @param {string} [config.username] - Username for CSRF auth (legacy)
47
- * @param {string} [config.password] - Password for CSRF auth (legacy)
48
- * @param {string} [config.title] - Custom run title (or use APPLIQATION_RUN_TITLE env var)
49
- * @param {Object} [config.options] - Additional options
50
- * @param {number} [config.options.timeout=30000] - Request timeout in ms
51
- * @param {number} [config.options.retries=3] - Number of retry attempts
52
- * @param {boolean} [config.options.logOrphans=true] - Log orphan tests to backend
53
- * @param {string} [config.options.logLevel='info'] - Logging level
26
+ * Resolve configuration from multiple sources.
27
+ * Priority: explicit config > env vars > defaults
28
+ *
29
+ * @param {Object} config - User-provided configuration
30
+ * @returns {Object} Resolved configuration
54
31
  */
55
- constructor(config) {
56
- // Apply SDK default for baseUrl when not provided
57
- const resolvedBaseUrl = (config?.baseUrl || process.env.APPLIQATION_BASE_URL || DEFAULT_APPLIQATION_BASE_URL || '').replace(/\/$/, '');
58
- const normalizedConfig = {
59
- ...(config || {}),
60
- baseUrl: resolvedBaseUrl
32
+ static resolveConfig(config = {}) {
33
+ const apiKey = config.apiKey || process.env.APPLIQATION_API_KEY || null;
34
+ const baseUrl = (config.baseUrl || DEFAULT_APPLIQATION_BASE_URL || '').replace(/\/$/, '');
35
+
36
+ return {
37
+ ...config,
38
+ apiKey,
39
+ baseUrl,
40
+ projectKey: config.projectKey || process.env.APPLIQATION_PROJECT_KEY || null,
41
+ runTitle: config.runTitle || config.title || process.env.APPLIQATION_RUN_TITLE || null,
61
42
  };
43
+ }
62
44
 
63
- // Validate configuration
64
- this.validateConfig(normalizedConfig);
45
+ /**
46
+ * Create Appliqation client
47
+ * @param {Object} config - Client configuration (can be empty — uses resolveConfig)
48
+ */
49
+ constructor(config = {}) {
50
+ const resolved = AppliqationClient.resolveConfig(config);
51
+
52
+ // Validate — warn instead of throw
53
+ const validation = this.validateConfig(resolved);
54
+ if (!validation.valid) {
55
+ // Log warnings but don't throw
56
+ for (const warning of validation.warnings) {
57
+ logger.warn(warning);
58
+ }
59
+ }
65
60
 
66
61
  // Determine SSL enforcement based on environment
67
62
  const isProduction = this._isProductionEnvironment();
68
- let rejectUnauthorized = true; // Default to secure
69
-
70
- if (normalizedConfig.rejectUnauthorized !== undefined) {
71
- // User explicitly set rejectUnauthorized
72
- if (isProduction && normalizedConfig.rejectUnauthorized === false) {
73
- // WARNING: User is trying to disable SSL in production
74
- logger.warn('SSL certificate verification is disabled in production environment!', {
75
- environment: process.env.NODE_ENV,
76
- ci: process.env.CI,
77
- warning: 'This is a security risk. SSL verification should be enabled in production.'
78
- });
63
+ let rejectUnauthorized = true;
64
+
65
+ if (resolved.rejectUnauthorized !== undefined) {
66
+ if (isProduction && resolved.rejectUnauthorized === false) {
67
+ logger.warn('SSL certificate verification is disabled in production environment!');
79
68
  }
80
- rejectUnauthorized = normalizedConfig.rejectUnauthorized;
81
- } else if (isProduction) {
82
- // Production environment - force SSL verification
83
- rejectUnauthorized = true;
84
- logger.info('SSL certificate verification enforced for production environment');
85
- } else {
86
- // Development environment - default to secure but can be overridden
87
- rejectUnauthorized = true;
69
+ rejectUnauthorized = resolved.rejectUnauthorized;
88
70
  }
89
71
 
90
72
  // Store configuration
91
73
  this.config = {
92
- baseUrl: resolvedBaseUrl,
93
- apiKey: normalizedConfig.apiKey,
94
- projectKey: normalizedConfig.projectKey,
95
- username: normalizedConfig.username,
96
- password: normalizedConfig.password,
97
- runTitle: normalizedConfig.runTitle || normalizedConfig.title || process.env.APPLIQATION_RUN_TITLE || null,
98
- rejectUnauthorized: rejectUnauthorized,
74
+ baseUrl: resolved.baseUrl,
75
+ apiKey: resolved.apiKey,
76
+ projectKey: resolved.projectKey,
77
+ username: resolved.username,
78
+ password: resolved.password,
79
+ runTitle: resolved.runTitle,
80
+ rejectUnauthorized,
99
81
  options: {
100
- timeout: normalizedConfig.options?.timeout || 30000,
101
- retries: normalizedConfig.options?.retries || 3,
102
- logOrphans: normalizedConfig.options?.logOrphans !== false,
103
- logLevel: normalizedConfig.options?.logLevel || 'info',
104
- // Auto-tagging configuration
105
- autoTag: normalizedConfig.options?.autoTag !== false,
106
- autoTagName: normalizedConfig.options?.autoTagName ||
107
- normalizedConfig.autoTagName ||
82
+ timeout: resolved.options?.timeout || 30000,
83
+ retries: resolved.options?.retries || 3,
84
+ logOrphans: resolved.options?.logOrphans !== false,
85
+ logLevel: resolved.options?.logLevel || 'info',
86
+ autoTag: resolved.options?.autoTag !== false,
87
+ autoTagName: resolved.options?.autoTagName ||
88
+ resolved.autoTagName ||
108
89
  process.env.APPLIQATION_AUTO_TAG_NAME ||
109
90
  'Appq_automated',
110
- autoTagBatchSize: normalizedConfig.options?.autoTagBatchSize || 50,
111
- autoTagRetries: normalizedConfig.options?.autoTagRetries || 2
91
+ autoTagBatchSize: resolved.options?.autoTagBatchSize || 50,
92
+ autoTagRetries: resolved.options?.autoTagRetries || 2
112
93
  }
113
94
  };
114
95
 
@@ -124,203 +105,64 @@ class AppliqationClient {
124
105
  this.tagging = new TaggingService(this.http, this.config);
125
106
  this.results = new ResultService(this.http, this.tagging, this.config);
126
107
  this.orphans = new OrphanTestService(this.http);
108
+ this.projectInfo = new ProjectInfoService();
127
109
 
128
- // Log tagging configuration
129
- logger.debug('TaggingService initialized', {
130
- enabled: this.tagging.isEnabled(),
131
- tagName: this.tagging.tagName
132
- });
110
+ // Auto-discovery state
111
+ this.availableEnvironments = null;
112
+ this._initialized = false;
133
113
 
134
114
  // Track current run context
135
115
  this.currentRun = null;
136
116
 
137
- // Auto-configuration state
138
- this._autoConfigured = false;
139
- this._projectInfo = null;
140
- this._autoConfigPromise = null; // Prevents concurrent auto-config calls
141
-
142
- // Show baseUrl only in DEBUG mode
143
117
  const meta = {
144
- authMode: this.config.apiKey ? 'api_key' : 'csrf',
145
- autoConfigEnabled: !this.config.projectKey // Will auto-fetch if projectKey not provided
118
+ authMode: this.config.apiKey ? 'api_key' : 'csrf'
146
119
  };
147
120
  logger.info('Appliqation client initialized', meta);
148
121
  logger.debug('Client configuration', { baseUrl: this.config.baseUrl });
149
122
  }
150
123
 
151
124
  /**
152
- * Ensure project configuration is loaded
153
- * Automatically fetches project metadata from API key if projectKey not provided.
154
- * This method is idempotent and can be called multiple times safely.
125
+ * Auto-discover project info from API key.
126
+ * Fills in missing projectKey and availableEnvironments.
127
+ * Never throws logs warnings on failure.
155
128
  *
156
- * @private
157
129
  * @returns {Promise<void>}
158
- * @throws {Error} If auto-configuration fails
159
130
  */
160
- async ensureProjectConfig() {
161
- // If projectKey provided manually, no need to fetch
162
- if (this.config.projectKey) {
163
- return;
164
- }
165
-
166
- // If already auto-configured, skip
167
- if (this._autoConfigured) {
168
- return;
169
- }
131
+ async initialize() {
132
+ if (this._initialized) return;
133
+ this._initialized = true;
170
134
 
171
- // If auto-config is in progress, wait for it
172
- if (this._autoConfigPromise) {
173
- return this._autoConfigPromise;
174
- }
135
+ if (!this.config.apiKey) return;
175
136
 
176
- // Start auto-configuration
177
- this._autoConfigPromise = this._performAutoConfig();
137
+ const info = await this.projectInfo.getProjectInfo(this.http);
178
138
 
179
- try {
180
- await this._autoConfigPromise;
181
- } finally {
182
- this._autoConfigPromise = null;
183
- }
184
- }
185
-
186
- /**
187
- * Perform auto-configuration by fetching project info
188
- * @private
189
- * @returns {Promise<void>}
190
- */
191
- async _performAutoConfig() {
192
- logger.info('Auto-fetching project configuration from API key...');
193
-
194
- try {
195
- this._projectInfo = await this.http.getProjectInfo();
196
-
197
- // Validate response structure
198
- if (!this._projectInfo || typeof this._projectInfo !== 'object') {
199
- throw new Error(
200
- `Invalid response format: expected object, got ${typeof this._projectInfo}`
201
- );
202
- }
203
-
204
- if (!this._projectInfo.project_id && this._projectInfo.project_id !== 0) {
205
- throw new Error(
206
- `Invalid response: missing project_id field in API response`
207
- );
139
+ if (info) {
140
+ // Auto-fill projectKey if not explicitly set
141
+ if (!this.config.projectKey && info.key) {
142
+ this.config.projectKey = info.key;
143
+ logger.info('Project auto-discovered', { projectKey: info.key, name: info.name });
208
144
  }
209
145
 
210
- this._autoConfigured = true;
211
-
212
- // Update config with fetched project_id
213
- this.config.projectKey = this._projectInfo.project_id.toString();
214
-
215
- logger.info('Auto-configuration successful', {
216
- project_id: this._projectInfo.project_id,
217
- project_title: this._projectInfo.project_title,
218
- environments: this._projectInfo.environments,
219
- default_environment: this._projectInfo.default_environment
220
- });
221
-
222
- // Log warning if no environments configured
223
- if (!this._projectInfo.has_environments_configured) {
224
- logger.warn('Project has no environments configured. Using default environment "Local".', {
225
- project_id: this._projectInfo.project_id,
226
- action_required: 'Configure environments in project settings for better environment validation'
227
- });
146
+ // Store available environments for validation
147
+ if (info.environments && info.environments.length > 0) {
148
+ this.availableEnvironments = info.environments;
149
+ logger.debug('Available environments', { environments: info.environments });
228
150
  }
229
- } catch (error) {
230
- logger.error('Auto-configuration failed', {
231
- error: error.message
232
- });
233
-
234
- throw new Error(
235
- `Failed to auto-configure project from API key: ${error.message}\n\n` +
236
- 'Possible solutions:\n' +
237
- '1. Verify your API key is valid and not expired\n' +
238
- '2. Ensure your API key is associated with a valid project\n' +
239
- '3. Check network connectivity to Appliqation instance\n' +
240
- '4. Provide projectKey manually in configuration as fallback:\n' +
241
- ' { projectKey: "your-project-id" }'
242
- );
151
+ } else if (!this.config.projectKey) {
152
+ console.warn('[Appliqation] Project auto-discovery failed. Tests will continue without reporting.');
243
153
  }
244
154
  }
245
155
 
246
- /**
247
- * Get available environments for the project
248
- * Automatically triggers project info fetch if not already loaded.
249
- *
250
- * @returns {Promise<Array<string>>} List of environment names
251
- * @example
252
- * const environments = await client.getAvailableEnvironments();
253
- * console.log(environments); // ['Development', 'Staging', 'Production']
254
- */
255
- async getAvailableEnvironments() {
256
- await this.ensureProjectConfig();
257
- return this._projectInfo?.environments || [];
258
- }
259
-
260
- /**
261
- * Get default environment for the project
262
- * Automatically triggers project info fetch if not already loaded.
263
- *
264
- * @returns {Promise<string>} Default environment name
265
- * @example
266
- * const defaultEnv = await client.getDefaultEnvironment();
267
- * console.log(defaultEnv); // 'Development'
268
- */
269
- async getDefaultEnvironment() {
270
- await this.ensureProjectConfig();
271
- return this._projectInfo?.default_environment || 'Local';
272
- }
273
-
274
- /**
275
- * Get cached project information
276
- * Returns null if auto-configuration hasn't been triggered yet.
277
- *
278
- * @returns {Object|null} Project metadata or null
279
- * @property {number} project_id - Project ID
280
- * @property {string} project_title - Project name
281
- * @property {Array<string>} environments - Available environments
282
- * @property {string} default_environment - Default environment
283
- * @property {boolean} has_environments_configured - Whether environments exist
284
- */
285
- getProjectInfo() {
286
- return this._projectInfo;
287
- }
288
-
289
156
  /**
290
157
  * Create a new test run matrix
291
- * @param {Object} options - Run configuration
292
- * @param {number} options.scenarioId - Scenario node ID
293
- * @param {number} options.testSetId - Test Set node ID (alternative to scenarioId)
294
- * @param {string} options.type - 'scenario' or 'testset'
295
- * @param {string} [options.environment='Local'] - Environment name (auto-detected if not provided)
296
- * @param {string[]} [options.browsers=['Chrome']] - Array of browser names
297
- * @param {string} [options.device] - Device type (Desktop, Mobile, Tablet)
298
- * @param {string} [options.os] - Operating system
299
- * @param {string} [options.title] - Custom run title
300
- * @returns {Promise<Object>} { runId, token, timestamp, metadata }
301
158
  */
302
159
  async createRun(options) {
303
160
  try {
304
- // Ensure project config is loaded (auto-fetch if needed)
305
- await this.ensureProjectConfig();
306
-
307
- // If environment not provided, use auto-detected default
308
- if (!options.environment && this._projectInfo) {
309
- options.environment = this._projectInfo.default_environment;
310
- logger.info('Using auto-detected default environment', {
311
- environment: options.environment,
312
- source: 'project_config'
313
- });
314
- }
315
-
316
161
  logger.info('Creating run matrix...', options);
317
-
318
162
  const run = await this.runMatrix.create(options);
319
163
 
320
- // Store current run context
321
164
  this.currentRun = run;
322
165
 
323
- // Store run configuration in AuthManager for JWT refresh capability
324
166
  if (this.auth && this.config.apiKey) {
325
167
  this.auth.setRunConfig({
326
168
  projectKey: this.config.projectKey,
@@ -332,7 +174,6 @@ class AppliqationClient {
332
174
  os: options.os,
333
175
  title: options.title || this.config.runTitle
334
176
  });
335
- logger.debug('Run configuration stored in AuthManager for JWT refresh');
336
177
  }
337
178
 
338
179
  logger.info('Run matrix created successfully', {
@@ -352,90 +193,56 @@ class AppliqationClient {
352
193
 
353
194
  /**
354
195
  * Create multiple runs for device/OS matrix testing
355
- * @param {Array} configs - Array of run configurations
356
- * @returns {Promise<Object>} { runs, errors, summary }
357
196
  */
358
197
  async createMultipleRuns(configs) {
359
198
  try {
360
199
  logger.info(`Creating ${configs.length} run matrices...`);
361
-
362
200
  const result = await this.runMatrix.createMultiple(configs);
363
-
364
201
  logger.info('Multiple runs created', {
365
202
  total: result.summary.total,
366
203
  success: result.summary.success,
367
204
  failed: result.summary.failed
368
205
  });
369
-
370
206
  return result;
371
207
  } catch (error) {
372
- logger.error('Failed to create multiple runs', {
373
- error: error.message
374
- });
208
+ logger.error('Failed to create multiple runs', { error: error.message });
375
209
  throw error;
376
210
  }
377
211
  }
378
212
 
379
213
  /**
380
214
  * Submit a single test result
381
- * @param {string} runId - Run ID
382
- * @param {Object} result - Test result
383
- * @param {string} result.uuid - Test case UUID
384
- * @param {string} result.status - Test status (passed, failed, skipped)
385
- * @param {string} result.browser - Browser name
386
- * @param {string} [result.parent_uuid] - Parent test case UUID
387
- * @param {string} [result.comment] - Additional comments
388
- * @param {string} [result.environment] - Environment name
389
- * @returns {Promise<Object>} Response from server
390
215
  */
391
216
  async submitResult(runId, result) {
392
217
  try {
393
- // Validate UUID
394
218
  const validation = UuidValidator.validateAndExtract(result.uuid);
395
219
  if (!validation.valid) {
396
220
  throw new Error(`Invalid UUID: ${validation.error}`);
397
221
  }
398
-
399
- // Validate result fields
400
222
  const resultValidation = PayloadBuilder.validateResult(result);
401
223
  if (!resultValidation.valid) {
402
224
  throw new Error(`Invalid result: ${resultValidation.errors.join(', ')}`);
403
225
  }
404
226
 
405
227
  logger.debug('Submitting test result', {
406
- runId,
407
- uuid: result.uuid,
408
- status: result.status,
409
- browser: result.browser
228
+ runId, uuid: result.uuid, status: result.status, browser: result.browser
410
229
  });
411
230
 
412
- const response = await this.results.submitSingle(runId, result);
413
-
414
- return response;
231
+ return await this.results.submitSingle(runId, result);
415
232
  } catch (error) {
416
233
  logger.error('Failed to submit result', {
417
- error: error.message,
418
- runId,
419
- uuid: result.uuid
234
+ error: error.message, runId, uuid: result.uuid
420
235
  });
421
236
  throw error;
422
237
  }
423
238
  }
424
239
 
425
240
  /**
426
- * Submit batch of test results with smart grouping
427
- * @param {Array} results - Array of test results
428
- * @param {Object} [options] - Batch options
429
- * @param {number} [options.batchSize=50] - Results per batch
430
- * @param {Function} [options.onProgress] - Progress callback
431
- * @param {boolean} [options.retryFailures=true] - Retry failed submissions
432
- * @returns {Promise<Object>} { success, failed, total }
241
+ * Submit batch of test results
433
242
  */
434
243
  async submitBatch(results, options = {}) {
435
244
  try {
436
245
  logger.info(`Submitting batch of ${results.length} results...`);
437
-
438
- // Validate all results first
439
246
  const validResults = [];
440
247
  const invalidResults = [];
441
248
 
@@ -445,10 +252,7 @@ class AppliqationClient {
445
252
  validResults.push(result);
446
253
  } else {
447
254
  invalidResults.push({ result, error: validation.error });
448
- logger.warn('Invalid UUID in batch', {
449
- uuid: result.uuid,
450
- error: validation.error
451
- });
255
+ logger.warn('Invalid UUID in batch', { uuid: result.uuid, error: validation.error });
452
256
  }
453
257
  }
454
258
 
@@ -459,317 +263,171 @@ class AppliqationClient {
459
263
  const summary = await this.results.submitBatch(validResults, options);
460
264
 
461
265
  logger.info('Batch submission completed', {
462
- success: summary.success,
463
- failed: summary.failed,
464
- invalid: invalidResults.length,
465
- total: results.length
266
+ success: summary.success, failed: summary.failed,
267
+ invalid: invalidResults.length, total: results.length
466
268
  });
467
269
 
468
- return {
469
- ...summary,
470
- invalid: invalidResults.length,
471
- invalidResults
472
- };
270
+ return { ...summary, invalid: invalidResults.length, invalidResults };
473
271
  } catch (error) {
474
- logger.error('Failed to submit batch', {
475
- error: error.message,
476
- count: results.length
477
- });
272
+ logger.error('Failed to submit batch', { error: error.message, count: results.length });
478
273
  throw error;
479
274
  }
480
275
  }
481
276
 
482
277
  /**
483
- * Log orphan tests (tests without UUID mappings)
484
- * @param {string} runId - Run ID
485
- * @param {Array} orphanTests - Array of orphan test objects
486
- * @returns {Promise<Object>} Response from server
278
+ * Log orphan tests
487
279
  */
488
280
  async logOrphanTests(runId, orphanTests) {
489
281
  try {
490
282
  if (!this.config.options.logOrphans) {
491
- logger.debug('Orphan test logging is disabled');
492
283
  return { success: true, logged: 0 };
493
284
  }
494
-
495
285
  if (!orphanTests || orphanTests.length === 0) {
496
286
  return { success: true, logged: 0 };
497
287
  }
498
288
 
499
289
  logger.info(`Logging ${orphanTests.length} orphan tests...`);
500
-
501
290
  const response = await this.orphans.log(runId, orphanTests);
502
-
503
- logger.info(`Orphan tests logged successfully`, {
504
- runId,
505
- count: orphanTests.length
506
- });
507
-
291
+ logger.info(`Orphan tests logged successfully`, { runId, count: orphanTests.length });
508
292
  return response;
509
293
  } catch (error) {
510
294
  logger.error('Failed to log orphan tests', {
511
- error: error.message,
512
- runId,
513
- count: orphanTests.length
295
+ error: error.message, runId, count: orphanTests.length
514
296
  });
515
- // Don't throw - orphan logging should not break the test run
516
297
  return { success: false, error: error.message };
517
298
  }
518
299
  }
519
300
 
520
301
  /**
521
- * Add browser to existing run matrix dynamically
522
- * @param {string} runId - Run ID
523
- * @param {string} browser - Browser name
524
- * @param {number} nid - Optional scenario/testset node ID (defaults to 0)
525
- * @returns {Promise<Object>} Response from server
302
+ * Add browser to existing run matrix
526
303
  */
527
304
  async addBrowser(runId, browser, nid = 0) {
528
305
  try {
529
306
  logger.info('Adding browser to run matrix', { runId, browser, nid });
530
-
531
307
  const response = await this.runMatrix.addBrowser(runId, browser, nid);
532
-
533
308
  logger.info('Browser added successfully', { runId, browser });
534
-
535
309
  return response;
536
310
  } catch (error) {
537
- logger.error('Failed to add browser', {
538
- error: error.message,
539
- runId,
540
- browser
541
- });
311
+ logger.error('Failed to add browser', { error: error.message, runId, browser });
542
312
  throw error;
543
313
  }
544
314
  }
545
315
 
546
316
  /**
547
317
  * Get run matrix data
548
- * @param {string} runId - Run ID
549
- * @returns {Promise<Object>} Run matrix data
550
318
  */
551
319
  async getRun(runId) {
552
320
  try {
553
- const response = await this.runMatrix.get(runId);
554
- return response;
321
+ return await this.runMatrix.get(runId);
555
322
  } catch (error) {
556
- logger.error('Failed to get run matrix', {
557
- error: error.message,
558
- runId
559
- });
323
+ logger.error('Failed to get run matrix', { error: error.message, runId });
560
324
  throw error;
561
325
  }
562
326
  }
563
327
 
564
- /**
565
- * Get current run context (if set)
566
- * @returns {Object|null} Current run data or null
567
- */
568
328
  getCurrentRun() {
569
329
  return this.currentRun;
570
330
  }
571
331
 
572
- /**
573
- * Set current run context manually
574
- * @param {Object} run - Run data with runId
575
- */
576
332
  setCurrentRun(run) {
577
333
  this.currentRun = run;
578
334
  }
579
335
 
580
336
  /**
581
337
  * Delete a test run
582
- * @param {string} runId - Run ID to delete
583
- * @param {string} reason - Deletion reason (for audit logging)
584
- * @returns {Promise<Object>} Deletion result
585
338
  */
586
339
  async deleteRun(runId, reason = 'orphan_cleanup') {
587
340
  try {
588
341
  logger.info('Deleting run...', { runId, reason });
589
-
590
342
  const result = await this.runMatrix.delete(runId, reason);
591
-
592
- // Clear from current run if it matches
593
343
  if (this.currentRun && this.currentRun.runId === runId) {
594
344
  this.currentRun = null;
595
345
  }
596
-
597
346
  logger.info('Run deleted successfully', { runId });
598
-
599
347
  return result;
600
348
  } catch (error) {
601
- logger.error('Failed to delete run', {
602
- error: error.message,
603
- runId
604
- });
349
+ logger.error('Failed to delete run', { error: error.message, runId });
605
350
  throw error;
606
351
  }
607
352
  }
608
353
 
609
- /**
610
- * Validate UUID format
611
- * @param {string} uuid - UUID to validate
612
- * @returns {boolean} True if valid
613
- */
614
354
  validateUuid(uuid) {
615
355
  return UuidValidator.validate(uuid);
616
356
  }
617
357
 
618
- /**
619
- * Extract NID from UUID
620
- * @param {string} uuid - UUID string
621
- * @returns {number|null} NID or null if invalid
622
- */
623
358
  extractNid(uuid) {
624
359
  return UuidValidator.extractNid(uuid);
625
360
  }
626
361
 
627
- /**
628
- * Validate and extract UUID components
629
- * @param {string} uuid - UUID string
630
- * @returns {Object} { valid, nid, uuid, error }
631
- */
632
362
  parseUuid(uuid) {
633
363
  return UuidValidator.validateAndExtract(uuid);
634
364
  }
635
365
 
636
366
  /**
637
- * Validate client configuration
367
+ * Validate client configuration — returns warnings instead of throwing.
638
368
  * @private
369
+ * @returns {{ valid: boolean, warnings: string[] }}
639
370
  */
640
371
  validateConfig(config) {
372
+ const warnings = [];
373
+
641
374
  if (!config) {
642
- throw new Error(
643
- 'Configuration is required.\n' +
644
- 'Please provide configuration object:\n' +
645
- ' const client = new AppliqationClient({\n' +
646
- ' baseUrl: process.env.APPLIQATION_BASE_URL,\n' +
647
- ' apiKey: process.env.APPLIQATION_API_KEY,\n' +
648
- ' projectKey: process.env.APPLIQATION_PROJECT_KEY\n' +
649
- ' });'
650
- );
375
+ warnings.push('[Appliqation] No configuration provided');
376
+ return { valid: false, warnings };
651
377
  }
652
378
 
653
- if (!config.baseUrl) {
654
- throw new Error(
655
- 'baseUrl is required.\n' +
656
- 'Please set the APPLIQATION_BASE_URL environment variable or provide baseUrl in configuration:\n' +
657
- ' APPLIQATION_BASE_URL=https://your-instance.appliqation.com\n' +
658
- 'Or in code:\n' +
659
- ' { baseUrl: "https://your-instance.appliqation.com" }'
660
- );
379
+ if (!config.apiKey) {
380
+ warnings.push('[Appliqation] API key required. Set APPLIQATION_API_KEY in .env');
661
381
  }
662
382
 
663
- // Must have either API key or username/password for authentication
664
- if (!config.apiKey && (!config.username || !config.password)) {
665
- throw new Error(
666
- 'Authentication credentials are required.\n' +
667
- 'Please set the APPLIQATION_API_KEY environment variable (recommended):\n' +
668
- ' APPLIQATION_API_KEY=appq_live_xxxxxxxxxxxx\n' +
669
- 'Or in code:\n' +
670
- ' { apiKey: "appq_live_xxxxxxxxxxxx" }\n' +
671
- '\n' +
672
- 'You can generate an API key in the Appliqation dashboard under Settings > API Keys.\n' +
673
- '\n' +
674
- 'Legacy CSRF authentication (username/password) is also supported but not recommended.'
675
- );
383
+ if (!config.projectKey) {
384
+ // Not an error will be auto-discovered
385
+ logger.debug('projectKey not set — will auto-discover from API key');
676
386
  }
677
387
 
678
- // projectKey is now optional when using API key authentication
679
- // If not provided, it will be auto-fetched from the API key's associated project
680
- if (!config.projectKey && !config.apiKey) {
681
- throw new Error(
682
- 'projectKey is required when not using API key authentication.\n' +
683
- 'Please set the APPLIQATION_PROJECT_KEY environment variable or provide projectKey in configuration:\n' +
684
- ' APPLIQATION_PROJECT_KEY=your-project-key\n' +
685
- 'Or in code:\n' +
686
- ' { projectKey: "your-project-key" }\n' +
687
- '\n' +
688
- 'Alternatively, use API key authentication (recommended) and projectKey will be auto-fetched:\n' +
689
- ' APPLIQATION_API_KEY=appq_live_xxxxxxxxxxxx\n' +
690
- '\n' +
691
- 'You can find your project key in the Appliqation dashboard under Project Settings.'
692
- );
388
+ if (config.baseUrl) {
389
+ try {
390
+ new URL(config.baseUrl);
391
+ } catch (error) {
392
+ warnings.push(`[Appliqation] Invalid baseUrl format: "${config.baseUrl}"`);
393
+ }
693
394
  }
694
395
 
695
- // Note: Both base64-encoded project keys AND numeric project IDs are supported
696
- // Numeric IDs are used when auto-fetched from API key
697
- // The backend validates project access permissions
698
-
699
- // Validate URL format
700
- try {
701
- new URL(config.baseUrl);
702
- } catch (error) {
703
- throw new Error(
704
- `Invalid baseUrl format: "${config.baseUrl}"\n` +
705
- 'baseUrl must be a valid URL starting with http:// or https://\n' +
706
- 'Example: https://your-instance.appliqation.com'
707
- );
708
- }
396
+ return { valid: warnings.length === 0, warnings };
709
397
  }
710
398
 
711
399
  /**
712
- * Test connectivity to Appliqation instance
713
- * @returns {Promise<boolean>} True if connection successful
400
+ * Test connectivity
714
401
  */
715
402
  async testConnection() {
716
403
  try {
717
404
  logger.info('Testing connection to Appliqation...');
718
-
719
- // Try to authenticate
720
405
  await this.auth.authenticate();
721
-
722
406
  logger.info('Connection test successful');
723
407
  return true;
724
408
  } catch (error) {
725
- logger.error('Connection test failed', {
726
- error: error.message
727
- });
409
+ logger.error('Connection test failed', { error: error.message });
728
410
  throw new Error(`Connection test failed: ${error.message}`);
729
411
  }
730
412
  }
731
413
 
732
414
  /**
733
- * Determine if the current environment is production
734
415
  * @private
735
- * @returns {boolean} True if production environment
736
416
  */
737
417
  _isProductionEnvironment() {
738
- // Check for CI environments
739
- if (process.env.CI === 'true' || process.env.CI === '1') {
740
- return true;
741
- }
742
-
743
- // Check NODE_ENV
418
+ if (process.env.CI === 'true' || process.env.CI === '1') return true;
744
419
  const nodeEnv = (process.env.NODE_ENV || '').toLowerCase();
745
- if (nodeEnv === 'production' || nodeEnv === 'prod' || nodeEnv === 'staging') {
746
- return true;
747
- }
748
-
749
- // Check for common CI environment variables
420
+ if (nodeEnv === 'production' || nodeEnv === 'prod' || nodeEnv === 'staging') return true;
750
421
  const ciEnvVars = [
751
- 'GITHUB_ACTIONS',
752
- 'GITLAB_CI',
753
- 'CIRCLECI',
754
- 'TRAVIS',
755
- 'JENKINS_URL',
756
- 'TEAMCITY_VERSION',
757
- 'BUILDKITE'
422
+ 'GITHUB_ACTIONS', 'GITLAB_CI', 'CIRCLECI', 'TRAVIS',
423
+ 'JENKINS_URL', 'TEAMCITY_VERSION', 'BUILDKITE'
758
424
  ];
759
-
760
425
  for (const envVar of ciEnvVars) {
761
- if (process.env[envVar]) {
762
- return true;
763
- }
426
+ if (process.env[envVar]) return true;
764
427
  }
765
-
766
428
  return false;
767
429
  }
768
430
 
769
- /**
770
- * Get SDK version
771
- * @returns {string} Version string
772
- */
773
431
  getVersion() {
774
432
  return require('../package.json').version;
775
433
  }