@appliqation/automation-sdk 2.7.0 → 2.8.0

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,11 +1,11 @@
1
1
  {
2
2
  "name": "@appliqation/automation-sdk",
3
- "version": "2.7.0",
4
- "description": "Appliqation Automation SDK — API key auth, custom run titles, framework reporters, portable storageState (setupAuth), and customer-defined login flows (defineLogin) for gated apps",
3
+ "version": "2.8.0",
4
+ "description": "Appliqation Automation SDK for Playwright — API-key auth, scope-validated result reporting, portable storageState (setupAuth), and customer-defined login flows (defineLogin) for gated apps",
5
5
  "main": "src/index.js",
6
6
  "types": "src/index.d.ts",
7
7
  "bin": {
8
- "appq-auth-setup": "./src/cli/auth-setup.js"
8
+ "appq-auth-setup": "src/cli/auth-setup.js"
9
9
  },
10
10
  "exports": {
11
11
  ".": {
@@ -32,14 +32,6 @@
32
32
  "require": "./src/playwright/fixture.js",
33
33
  "import": "./src/playwright/fixture.js"
34
34
  },
35
- "./cypress": {
36
- "require": "./src/reporters/cypress/index.js",
37
- "import": "./src/reporters/cypress/index.js"
38
- },
39
- "./jest": {
40
- "require": "./src/reporters/jest/index.js",
41
- "import": "./src/reporters/jest/index.js"
42
- },
43
35
  "./utils": {
44
36
  "require": "./src/utils/index.js",
45
37
  "import": "./src/utils/index.js"
@@ -65,9 +57,7 @@
65
57
  "lint": "eslint src/",
66
58
  "lint:fix": "eslint src/ --fix",
67
59
  "example:basic": "node examples/basic-usage.js",
68
- "example:playwright": "cd examples/playwright-basic && npx playwright test",
69
- "example:cypress": "cd examples/cypress-basic && npm test",
70
- "example:jest": "cd examples/jest-basic && npm test",
60
+ "example:playwright": "cd examples/playwright && npx playwright test",
71
61
  "docs": "echo 'Documentation generation coming soon'"
72
62
  },
73
63
  "keywords": [
@@ -76,10 +66,6 @@
76
66
  "automation",
77
67
  "test-management",
78
68
  "playwright",
79
- "cypress",
80
- "jest",
81
- "selenium",
82
- "webdriver",
83
69
  "api-key",
84
70
  "sdk",
85
71
  "test-reporting",
@@ -91,31 +77,16 @@
91
77
  "license": "MIT",
92
78
  "dependencies": {
93
79
  "axios": "^1.6.0",
94
- "dotenv": "^16.3.1",
95
80
  "jsonwebtoken": "^9.0.3"
96
81
  },
97
82
  "devDependencies": {
98
83
  "@playwright/test": "^1.40.0",
99
- "cypress": "^13.0.0",
100
84
  "eslint": "^8.57.0",
101
85
  "jest": "^29.7.0",
102
86
  "playwright": "^1.40.0"
103
87
  },
104
88
  "peerDependencies": {
105
- "@playwright/test": ">=1.30.0",
106
- "cypress": ">=10.0.0",
107
- "jest": ">=29.0.0"
108
- },
109
- "peerDependenciesMeta": {
110
- "@playwright/test": {
111
- "optional": true
112
- },
113
- "cypress": {
114
- "optional": true
115
- },
116
- "jest": {
117
- "optional": true
118
- }
89
+ "@playwright/test": ">=1.30.0"
119
90
  },
120
91
  "engines": {
121
92
  "node": ">=18.0.0"
@@ -8,7 +8,13 @@ const ProjectInfoService = require('./services/ProjectInfoService');
8
8
  const UuidValidator = require('./utils/UuidValidator');
9
9
  const PayloadBuilder = require('./utils/PayloadBuilder');
10
10
  const logger = require('./utils/logger');
11
- const { DEFAULT_APPLIQATION_BASE_URL } = require('./constants');
11
+ const { ConfigurationError } = require('./utils/errors');
12
+ const { DEFAULT_APPLIQATION_BASE_URL, AUTO_TAG_NAME } = require('./constants');
13
+
14
+ // H2 — environment names must match the backend's InputValidator regex
15
+ // (appq_mongo InputValidator). Mismatch causes a 400 mid-run; reject at
16
+ // startup instead so misconfiguration fails immediately and obviously.
17
+ const ENVIRONMENT_NAME_PATTERN = /^[a-zA-Z0-9._-]+$/;
12
18
 
13
19
  /**
14
20
  * Main Appliqation Automation SDK Client
@@ -49,22 +55,51 @@ class AppliqationClient {
49
55
  constructor(config = {}) {
50
56
  const resolved = AppliqationClient.resolveConfig(config);
51
57
 
52
- // Validatewarn instead of throw
58
+ // H2fail-fast schema validation. Hard errors throw at construction so
59
+ // misconfiguration surfaces immediately instead of after a full test run.
60
+ // Soft warnings (missing apiKey, etc.) still just log so no-op mode is
61
+ // preserved when the user intentionally runs without reporting.
53
62
  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
- }
63
+ if (validation.errors.length > 0) {
64
+ throw new ConfigurationError(
65
+ `Invalid Appliqation SDK configuration:\n - ${validation.errors.join('\n - ')}`,
66
+ { errors: validation.errors }
67
+ );
68
+ }
69
+ for (const warning of validation.warnings) {
70
+ logger.warn(warning);
59
71
  }
60
72
 
61
- // Determine SSL enforcement based on environment
73
+ // H3 TLS enforcement. Disabling cert validation is allowed in local
74
+ // dev (self-signed certs) but actively dangerous in CI/production: a
75
+ // MITM attacker can intercept result submissions and alter verdicts.
76
+ // The previous behaviour only warned; we now throw to make the misuse
77
+ // impossible to ship. Customers who genuinely need to bypass TLS in a
78
+ // CI environment must explicitly set APPLIQATION_INSECURE=allow.
62
79
  const isProduction = this._isProductionEnvironment();
63
80
  let rejectUnauthorized = true;
64
81
 
65
82
  if (resolved.rejectUnauthorized !== undefined) {
66
83
  if (isProduction && resolved.rejectUnauthorized === false) {
67
- logger.warn('SSL certificate verification is disabled in production environment!');
84
+ const overrideAllowed = process.env.APPLIQATION_INSECURE === 'allow';
85
+ if (!overrideAllowed) {
86
+ throw new ConfigurationError(
87
+ 'rejectUnauthorized=false is not permitted in CI/production. '
88
+ + 'Disabling TLS verification exposes result submissions to MITM. '
89
+ + 'Use a properly trusted certificate, or set '
90
+ + 'APPLIQATION_INSECURE=allow to acknowledge the risk and override.',
91
+ {
92
+ detectedAs: 'production',
93
+ ci: process.env.CI,
94
+ nodeEnv: process.env.NODE_ENV
95
+ }
96
+ );
97
+ }
98
+ logger.warn(
99
+ 'TLS verification is DISABLED via APPLIQATION_INSECURE=allow in a '
100
+ + 'production-like environment. Result submissions are vulnerable '
101
+ + 'to interception.'
102
+ );
68
103
  }
69
104
  rejectUnauthorized = resolved.rejectUnauthorized;
70
105
  }
@@ -84,10 +119,8 @@ class AppliqationClient {
84
119
  logOrphans: resolved.options?.logOrphans !== false,
85
120
  logLevel: resolved.options?.logLevel || 'info',
86
121
  autoTag: resolved.options?.autoTag !== false,
87
- autoTagName: resolved.options?.autoTagName ||
88
- resolved.autoTagName ||
89
- process.env.APPLIQATION_AUTO_TAG_NAME ||
90
- 'Appq_automated',
122
+ // Fixed, not configurable — see AUTO_TAG_NAME's doc comment.
123
+ autoTagName: AUTO_TAG_NAME,
91
124
  autoTagBatchSize: resolved.options?.autoTagBatchSize || 50,
92
125
  autoTagRetries: resolved.options?.autoTagRetries || 2
93
126
  }
@@ -351,6 +384,20 @@ class AppliqationClient {
351
384
  }
352
385
  }
353
386
 
387
+ /**
388
+ * Fetch the UUID membership of a Test Set, for scope validation.
389
+ */
390
+ async fetchTestSetScope(testSetId) {
391
+ try {
392
+ const response = await this.http.get(`/api/automation/testset/${testSetId}/scope`);
393
+ const uuids = response?.data?.data;
394
+ return new Set(Array.isArray(uuids) ? uuids : []);
395
+ } catch (error) {
396
+ logger.error('Failed to fetch testset scope', { error: error.message, testSetId });
397
+ throw error;
398
+ }
399
+ }
400
+
354
401
  validateUuid(uuid) {
355
402
  return UuidValidator.validate(uuid);
356
403
  }
@@ -364,36 +411,89 @@ class AppliqationClient {
364
411
  }
365
412
 
366
413
  /**
367
- * Validate client configuration — returns warnings instead of throwing.
414
+ * Validate client configuration.
415
+ *
416
+ * Hard errors (returned in `errors`) cause the constructor to throw
417
+ * ConfigurationError. They cover misconfiguration that would otherwise
418
+ * fail silently or mid-run: malformed URL, non-positive numerics, invalid
419
+ * environment name format. Soft warnings (returned in `warnings`) are
420
+ * logged but don't block construction — missing apiKey, etc.
421
+ *
368
422
  * @private
369
- * @returns {{ valid: boolean, warnings: string[] }}
423
+ * @returns {{ errors: string[], warnings: string[] }}
370
424
  */
371
425
  validateConfig(config) {
426
+ const errors = [];
372
427
  const warnings = [];
373
428
 
374
429
  if (!config) {
375
- warnings.push('[Appliqation] No configuration provided');
376
- return { valid: false, warnings };
430
+ errors.push('No configuration object provided');
431
+ return { errors, warnings };
377
432
  }
378
433
 
434
+ // apiKey — warn only (no-op mode is intentional behaviour)
379
435
  if (!config.apiKey) {
380
436
  warnings.push('[Appliqation] API key required. Set APPLIQATION_API_KEY in .env');
437
+ } else if (typeof config.apiKey !== 'string' || config.apiKey.trim() === '') {
438
+ errors.push(`apiKey must be a non-empty string (got: ${typeof config.apiKey})`);
381
439
  }
382
440
 
383
- if (!config.projectKey) {
384
- // Not an error will be auto-discovered
385
- logger.debug('projectKey not set — will auto-discover from API key');
386
- }
387
-
441
+ // baseUrl — hard error on malformed URL (silent failure otherwise:
442
+ // every HTTP call fails with a cryptic axios error mid-run).
388
443
  if (config.baseUrl) {
389
444
  try {
390
- new URL(config.baseUrl);
445
+ const url = new URL(config.baseUrl);
446
+ if (!['http:', 'https:'].includes(url.protocol)) {
447
+ errors.push(`baseUrl must use http or https (got: "${url.protocol}")`);
448
+ }
391
449
  } catch (error) {
392
- warnings.push(`[Appliqation] Invalid baseUrl format: "${config.baseUrl}"`);
450
+ errors.push(`Invalid baseUrl: "${config.baseUrl}" is not a valid URL`);
451
+ }
452
+ }
453
+
454
+ // environment — hard error on invalid format (backend rejects 400 mid-run)
455
+ if (config.environment !== undefined && config.environment !== null && config.environment !== '') {
456
+ if (typeof config.environment !== 'string') {
457
+ errors.push(`environment must be a string (got: ${typeof config.environment})`);
458
+ } else if (!ENVIRONMENT_NAME_PATTERN.test(config.environment)) {
459
+ errors.push(
460
+ `environment "${config.environment}" contains invalid characters. `
461
+ + 'Allowed: letters, digits, dot, underscore, hyphen.'
462
+ );
463
+ }
464
+ }
465
+
466
+ // options.timeout — must be a positive integer if specified
467
+ if (config.options && config.options.timeout !== undefined) {
468
+ const t = config.options.timeout;
469
+ if (!Number.isInteger(t) || t <= 0) {
470
+ errors.push(`options.timeout must be a positive integer (got: ${t})`);
471
+ }
472
+ }
473
+
474
+ // options.retries — must be a non-negative integer if specified
475
+ if (config.options && config.options.retries !== undefined) {
476
+ const r = config.options.retries;
477
+ if (!Number.isInteger(r) || r < 0) {
478
+ errors.push(`options.retries must be a non-negative integer (got: ${r})`);
479
+ }
480
+ }
481
+
482
+ // options.batchSize / options.autoTagBatchSize — must be positive integers
483
+ for (const field of ['batchSize', 'autoTagBatchSize']) {
484
+ if (config.options && config.options[field] !== undefined) {
485
+ const v = config.options[field];
486
+ if (!Number.isInteger(v) || v <= 0) {
487
+ errors.push(`options.${field} must be a positive integer (got: ${v})`);
488
+ }
393
489
  }
394
490
  }
395
491
 
396
- return { valid: warnings.length === 0, warnings };
492
+ if (!config.projectKey) {
493
+ logger.debug('projectKey not set — will auto-discover from API key');
494
+ }
495
+
496
+ return { errors, warnings };
397
497
  }
398
498
 
399
499
  /**
@@ -8,7 +8,8 @@
8
8
  *
9
9
  * Customer setup (one-time, per project):
10
10
  * 1. Configure roles in Appliqation Project Settings → Auth Config
11
- * 2. Write `tests/appliqation/auth/login.ts` in their repo:
11
+ * 2. Write `tests/automan/auth/login.ts` in their repo (default path;
12
+ * configurable in Project Settings):
12
13
  *
13
14
  * import { defineLogin } from '@appliqation/automation-sdk/login';
14
15
  * export default defineLogin(async (page, { username, password }) => {
@@ -50,7 +51,7 @@ const {
50
51
  } = require('../utils/setupAuth');
51
52
 
52
53
  const FATAL_EXIT = 1;
53
- const DEFAULT_LOGIN_FILE = 'tests/appliqation/auth/login.ts';
54
+ const DEFAULT_LOGIN_FILE = 'tests/automan/auth/login.ts';
54
55
 
55
56
  function parseArgs(argv) {
56
57
  const args = {};
@@ -78,12 +79,21 @@ function fail(message) {
78
79
  * Resolve the customer's login.ts path. Priority:
79
80
  * 1. --login-file CLI flag (explicit override)
80
81
  * 2. APPQ_LOGIN_FILE env var (CI override without changing CLI args)
81
- * 3. Convention: tests/appliqation/auth/login.ts relative to CWD
82
+ * 3. Convention: tests/automan/auth/login.ts relative to CWD
82
83
  *
83
84
  * Throws clearly if the resolved path doesn't exist — customers who
84
85
  * haven't created the file yet need a path-specific error, not a
85
86
  * cryptic dynamic-import failure.
86
87
  */
88
+ // H4 — only allow loading login scripts written in JS or TS. Blocks an
89
+ // attacker from convincing the CLI to require() arbitrary file types
90
+ // (.json with prototype pollution, .node native addons, .yaml via a
91
+ // custom hook, etc).
92
+ const ALLOWED_LOGIN_FILE_EXTENSIONS = new Set([
93
+ '.ts', '.tsx', '.mts', '.cts',
94
+ '.js', '.mjs', '.cjs', '.jsx'
95
+ ]);
96
+
87
97
  function resolveLoginFile(args) {
88
98
  const candidate = args['login-file']
89
99
  || process.env.APPQ_LOGIN_FILE
@@ -91,9 +101,48 @@ function resolveLoginFile(args) {
91
101
  const absolute = path.isAbsolute(candidate)
92
102
  ? candidate
93
103
  : path.resolve(process.cwd(), candidate);
94
- if (!fs.existsSync(absolute)) {
104
+
105
+ // H4 — directory-traversal guard. The CLI loads the resolved file via
106
+ // require() / dynamic import(), so a path outside the project root is
107
+ // an arbitrary-code-execution vector in a compromised CI environment
108
+ // (malicious PR, supply chain). Require the login file to live inside
109
+ // the current working directory tree.
110
+ const projectRoot = process.cwd();
111
+ const rootWithSep = projectRoot.endsWith(path.sep) ? projectRoot : projectRoot + path.sep;
112
+ // Realpath to defeat symlink escapes; fall back to absolute if the
113
+ // file doesn't exist yet so the existence error below still fires
114
+ // with the user's intended path.
115
+ let canonical;
116
+ try {
117
+ canonical = fs.realpathSync(absolute);
118
+ } catch (e) {
119
+ canonical = absolute;
120
+ }
121
+ if (canonical !== projectRoot && !canonical.startsWith(rootWithSep)) {
95
122
  fail(
96
- `Login file not found: ${absolute}\n`
123
+ `Login file must live inside the project directory.\n`
124
+ + ` Project root: ${projectRoot}\n`
125
+ + ` Login file: ${canonical}\n`
126
+ + `\n`
127
+ + ` Resolving outside the project tree would let any process that\n`
128
+ + ` controls --login-file or APPQ_LOGIN_FILE load arbitrary code\n`
129
+ + ` through this CLI. Move the file under the project root.`
130
+ );
131
+ }
132
+
133
+ // H4 — extension allowlist.
134
+ const ext = path.extname(canonical).toLowerCase();
135
+ if (!ALLOWED_LOGIN_FILE_EXTENSIONS.has(ext)) {
136
+ fail(
137
+ `Login file extension "${ext}" is not allowed.\n`
138
+ + ` Allowed: ${Array.from(ALLOWED_LOGIN_FILE_EXTENSIONS).join(', ')}\n`
139
+ + ` Got: ${canonical}`
140
+ );
141
+ }
142
+
143
+ if (!fs.existsSync(canonical)) {
144
+ fail(
145
+ `Login file not found: ${canonical}\n`
97
146
  + ` Create it (default path: ${DEFAULT_LOGIN_FILE}) and export your login flow as a default export:\n`
98
147
  + `\n`
99
148
  + ` import { defineLogin } from '@appliqation/automation-sdk/login';\n`
@@ -103,7 +152,7 @@ function resolveLoginFile(args) {
103
152
  + ` });\n`
104
153
  );
105
154
  }
106
- return absolute;
155
+ return canonical;
107
156
  }
108
157
 
109
158
  /**
@@ -220,7 +269,13 @@ async function main() {
220
269
  }
221
270
 
222
271
  const targetPath = authStatePath({ project_id, role });
223
- fs.mkdirSync(path.dirname(targetPath), { recursive: true });
272
+ // H1 restrict the auth-state directory to owner-only. Playwright's
273
+ // storageState file contains cookies, localStorage, and sessionStorage
274
+ // — equivalent to a live authenticated browser session. Default
275
+ // mkdir permissions inherit the umask (typically 0755 / world-readable
276
+ // on shared CI runners and developer machines); force 0700 so no
277
+ // other local user can read peer sessions.
278
+ fs.mkdirSync(path.dirname(targetPath), { recursive: true, mode: 0o700 });
224
279
 
225
280
  console.log(`→ Launching Chromium for login as role "${role}" against ${baseURL}`);
226
281
  const browser = await chromium.launch({ headless: true });
@@ -233,7 +288,15 @@ async function main() {
233
288
  try {
234
289
  await loginFn(page, { username, password, role, baseURL });
235
290
  await context.storageState({ path: targetPath });
236
- console.log(`✓ Auth state saved to ${targetPath}`);
291
+ // H1 Playwright writes storageState with umask-inherited
292
+ // permissions (typically 0644). Tighten to 0600 so even other
293
+ // local users on the same machine cannot read it.
294
+ try {
295
+ fs.chmodSync(targetPath, 0o600);
296
+ } catch (chmodErr) {
297
+ console.warn(`⚠️ Could not tighten permissions on ${targetPath}: ${chmodErr.message}`);
298
+ }
299
+ console.log(`✓ Auth state saved to ${targetPath} (mode 0600)`);
237
300
  console.log(` Tests using setupAuth({ project_id: ${project_id}, role: '${role}' }) will pick this up.`);
238
301
  } catch (err) {
239
302
  fail(`Login failed: ${err.message}`);
package/src/constants.js CHANGED
@@ -204,6 +204,23 @@ const DEFAULT_OS = 'Unknown';
204
204
  */
205
205
  const DEFAULT_APPLIQATION_BASE_URL = 'https://appq.appliqation.io';
206
206
 
207
+ // ============================================================================
208
+ // Auto-Tagging Configuration
209
+ // ============================================================================
210
+
211
+ /**
212
+ * Tag applied to a test case the first time the SDK submits an accepted
213
+ * result for it. Marks the test case as automation-covered.
214
+ *
215
+ * Fixed, not configurable — appq's own backend (AutomationResultManager's
216
+ * autoTagScenario/autoTagTestset) writes this exact literal when it
217
+ * auto-tags at run creation. A configurable client-side name would let the
218
+ * SDK and backend disagree and produce two different tags for the same
219
+ * "automated" signal.
220
+ * @constant {string}
221
+ */
222
+ const AUTO_TAG_NAME = 'Appq_Auto';
223
+
207
224
  // ============================================================================
208
225
  // Exports
209
226
  // ============================================================================
@@ -251,5 +268,8 @@ module.exports = {
251
268
  DEFAULT_OS,
252
269
 
253
270
  // Base URL
254
- DEFAULT_APPLIQATION_BASE_URL
271
+ DEFAULT_APPLIQATION_BASE_URL,
272
+
273
+ // Auto-Tagging
274
+ AUTO_TAG_NAME
255
275
  };
@@ -180,6 +180,11 @@ class AuthManager {
180
180
 
181
181
  parseJwtExpiry(token) {
182
182
  try {
183
+ // Signature not verified — token is issued by the Appliqation API server
184
+ // we just authenticated against, so origin is trusted. We only decode
185
+ // the payload to read `exp` for refresh scheduling. Verifying the HMAC
186
+ // here would require the server-side signing secret, which the SDK
187
+ // does not (and must not) hold.
183
188
  const payload = JSON.parse(Buffer.from(token.split('.')[1], 'base64').toString());
184
189
  // Convert to milliseconds and subtract 5 minutes for safety margin
185
190
  return (payload.exp * 1000) - (5 * 60 * 1000);
package/src/index.d.ts CHANGED
@@ -1,8 +1,20 @@
1
1
  /**
2
2
  * TypeScript definitions for @appliqation/automation-sdk
3
- * @version 2.1.0
3
+ * @version 2.8.0
4
4
  */
5
5
 
6
+ /**
7
+ * Scope-mismatch resolution strategy (audit finding C5).
8
+ *
9
+ * When `scenarioId` or `testSetId` is set in config and the executed
10
+ * suite contains UUIDs outside that scope, the SDK applies one of
11
+ * these strategies before submitting results.
12
+ * 'cancel' — fail fast, submit nothing, exit 1 (default in CI)
13
+ * 'adhoc' — override config, create an ad-hoc run, submit everything
14
+ * 'filter' — submit only in-scope results, drop the rest locally
15
+ */
16
+ export type ScopeMismatchStrategy = 'cancel' | 'adhoc' | 'filter';
17
+
6
18
  // ============================================================================
7
19
  // Core Types
8
20
  // ============================================================================
@@ -49,10 +61,16 @@ export interface AppliqationConfig {
49
61
  /** Project key */
50
62
  projectKey?: string;
51
63
 
52
- /** Username for legacy CSRF authentication */
64
+ /**
65
+ * @deprecated Legacy CSRF authentication. Slated for removal in a
66
+ * future major version (audit finding D4). Use `apiKey` instead.
67
+ */
53
68
  username?: string;
54
69
 
55
- /** Password for legacy CSRF authentication */
70
+ /**
71
+ * @deprecated Legacy CSRF authentication. Slated for removal in a
72
+ * future major version (audit finding D4). Use `apiKey` instead.
73
+ */
56
74
  password?: string;
57
75
 
58
76
  /** Scenario ID (optional, 0 for generic automation runs) */
@@ -88,9 +106,25 @@ export interface AppliqationConfig {
88
106
  /** Batch size for result submission */
89
107
  batchSize?: number;
90
108
 
91
- /** SSL certificate verification (default: true) */
109
+ /**
110
+ * SSL certificate verification (default: true).
111
+ *
112
+ * H3 — setting this to `false` in a production-like environment
113
+ * (`CI=true`, `NODE_ENV=production`/`prod`/`staging`, or any well-known
114
+ * CI env var) will throw `ConfigurationError` at client construction.
115
+ * Explicit override via `APPLIQATION_INSECURE=allow` for the rare case
116
+ * where a customer genuinely needs to bypass TLS in CI.
117
+ */
92
118
  rejectUnauthorized?: boolean;
93
119
 
120
+ /**
121
+ * C5 — scope-mismatch resolution strategy. Applied when the executed
122
+ * suite contains UUIDs outside the configured `scenarioId`/`testSetId`.
123
+ * Also settable via `APPLIQATION_ON_SCOPE_MISMATCH` env var.
124
+ * Defaults to 'cancel' in CI (fail safe), interactive prompt in TTY.
125
+ */
126
+ onScopeMismatch?: ScopeMismatchStrategy;
127
+
94
128
  /** Additional options */
95
129
  options?: {
96
130
  /** Request timeout in milliseconds (default: 30000) */
@@ -232,9 +266,16 @@ export class AppliqationClient {
232
266
  constructor(config: AppliqationConfig);
233
267
 
234
268
  /**
235
- * Create a new automation run
269
+ * Create a new automation run.
270
+ *
271
+ * The optional `uuids` field (C4 / E3) pre-populates the run
272
+ * document's `data[]` array so the UI grid and orphan detector know
273
+ * which TCs belong to the run before any result lands. Pass plain
274
+ * string UUIDs matching PR appq/#532's schema (not `{uuid}` objects).
236
275
  */
237
- createRun(config?: Partial<AppliqationConfig>): Promise<RunMatrixResponse>;
276
+ createRun(
277
+ config?: Partial<AppliqationConfig> & { uuids?: string[] }
278
+ ): Promise<RunMatrixResponse>;
238
279
 
239
280
  /**
240
281
  * Submit a single test result
@@ -299,6 +340,61 @@ export class PayloadBuilder {
299
340
  status: TestStatus,
300
341
  options?: { comment?: string; attachments?: string[]; metadata?: Record<string, any> }
301
342
  ): TestResult;
343
+
344
+ /**
345
+ * S8 — validate a fully assembled automation result payload against
346
+ * backend shape constraints (InputValidator regexes + status enum).
347
+ * Runs client-side immediately before submission so schema drift is
348
+ * caught with a precise field-path error instead of a generic 400.
349
+ */
350
+ static validateAutomationResultPayload(payload: {
351
+ run_id?: unknown;
352
+ test_case_uuid?: unknown;
353
+ status?: unknown;
354
+ source?: unknown;
355
+ browser?: unknown;
356
+ environment?: unknown;
357
+ duration?: unknown;
358
+ timestamp?: unknown;
359
+ [key: string]: unknown;
360
+ }): { valid: boolean; errors: string[] };
361
+ }
362
+
363
+ /**
364
+ * C5 — Pre-execution scope validator. Verifies UUIDs about to execute
365
+ * match the configured scenarioId/testSetId; applies cancel/adhoc/filter
366
+ * strategy on mismatch. Wired automatically by AppliqationReporter in
367
+ * `onBegin`; exposed for programmatic use.
368
+ */
369
+ export class ScopeValidator {
370
+ constructor(options: {
371
+ scenarioId?: number | string;
372
+ testSetId?: number | string;
373
+ strategy?: ScopeMismatchStrategy;
374
+ fetchTestSetScope?: (testSetId: number | string) => Promise<Set<string> | string[]>;
375
+ });
376
+
377
+ static readonly STRATEGY: {
378
+ CANCEL: 'cancel';
379
+ ADHOC: 'adhoc';
380
+ FILTER: 'filter';
381
+ };
382
+
383
+ isScoped(): boolean;
384
+ extractUuidsFromSuite(suite: unknown): string[];
385
+ validate(uuids: string[]): Promise<{
386
+ hasMismatch: boolean;
387
+ inScope: string[];
388
+ outOfScope: Array<{ uuid: string; actualScenarioId?: number }>;
389
+ reason: 'scenario' | 'testset' | null;
390
+ }>;
391
+ resolve(validation: unknown): Promise<{
392
+ action: 'cancel' | 'adhoc' | 'filter' | 'pass';
393
+ submitUuids: string[];
394
+ droppedUuids: string[];
395
+ overrideToAdhoc: boolean;
396
+ message: string;
397
+ }>;
302
398
  }
303
399
 
304
400
  export const logger: {
@@ -1,7 +1,8 @@
1
1
  /**
2
2
  * TypeScript definitions for @appliqation/automation-sdk/login.
3
3
  *
4
- * Customers writing `tests/appliqation/auth/login.ts` import these
4
+ * Customers writing `tests/automan/auth/login.ts` (default path;
5
+ * configurable in Project Settings) import these
5
6
  * types for autocomplete + compile-time checking on the login flow
6
7
  * function shape. Same file gets imported (untyped, dynamically) by
7
8
  * the Appliqation executor's LoginHelper and the appq-auth-setup CLI.
@@ -3,7 +3,8 @@
3
3
  * supplied login flows.
4
4
  *
5
5
  * Customers define their SUT's login flow as a Playwright function in
6
- * their own repo (canonical path: `tests/appliqation/auth/login.ts`).
6
+ * their own repo (canonical path: `tests/automan/auth/login.ts`, default;
7
+ * configurable in Project Settings).
7
8
  * The function is imported by:
8
9
  * 1. Appliqation's executor (LoginHelper) — pulled from MongoDB
9
10
  * after a GitHub-webhook upsert into automan_canonical_scripts
@@ -9,7 +9,10 @@ try {
9
9
  }
10
10
 
11
11
  const JwtBrowserAuth = require('./JwtBrowserAuth');
12
- require('dotenv').config();
12
+ // L3 — dotenv removed from the fixture. In enterprise CI, env vars are
13
+ // injected by the platform (GitHub Actions, Jenkins, GitLab); a stray
14
+ // committed .env file would otherwise silently override them. Env var
15
+ // loading is the customer's responsibility in playwright.config.ts.
13
16
  const { DEFAULT_APPLIQATION_BASE_URL } = require('../constants');
14
17
 
15
18
  /**