@msalaam/xray-qe-toolkit 1.5.0 → 1.6.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.
package/lib/xrayClient.js CHANGED
@@ -90,6 +90,9 @@ export async function createIssue(cfg, issueType, fields) {
90
90
  issuetype: { name: issueType },
91
91
  ...(fields.priority ? { priority: { name: fields.priority } } : {}),
92
92
  ...(fields.labels && fields.labels.length > 0 ? { labels: fields.labels } : {}),
93
+ ...(fields.fixVersions && fields.fixVersions.length > 0
94
+ ? { fixVersions: fields.fixVersions.map((v) => ({ name: v })) }
95
+ : {}),
93
96
  },
94
97
  };
95
98
 
@@ -147,11 +150,25 @@ export async function getIssue(cfg, issueKey) {
147
150
  * @returns {Promise<object>} data object
148
151
  */
149
152
  async function graphql(cfg, xrayToken, query, variables = {}) {
150
- const response = await axios.post(
151
- cfg.xrayGraphqlUrl,
152
- { query, variables },
153
- { httpsAgent, headers: xrayHeaders(xrayToken) }
154
- );
153
+ let response;
154
+ try {
155
+ response = await axios.post(
156
+ cfg.xrayGraphqlUrl,
157
+ { query, variables },
158
+ { httpsAgent, headers: xrayHeaders(xrayToken) }
159
+ );
160
+ } catch (err) {
161
+ // Extract the actual response body from axios HTTP errors (4xx/5xx)
162
+ if (err.response) {
163
+ const body = err.response.data
164
+ ? (typeof err.response.data === "string"
165
+ ? err.response.data
166
+ : JSON.stringify(err.response.data))
167
+ : "(no body)";
168
+ throw new Error(`HTTP ${err.response.status} from Xray GraphQL: ${body}`);
169
+ }
170
+ throw err;
171
+ }
155
172
 
156
173
  if (response.data.errors) {
157
174
  throw new Error(`GraphQL errors: ${JSON.stringify(response.data.errors)}`);
@@ -219,19 +236,22 @@ export async function getTests(cfg, xrayToken, opts = {}) {
219
236
  }
220
237
 
221
238
  /**
222
- * Get a Test Plan by issue ID (includes its tests).
239
+ * Get a Test Plan by issue ID (includes its tests, paginated).
223
240
  * @param {object} cfg
224
241
  * @param {string} xrayToken
225
242
  * @param {string} issueId
243
+ * @param {object} [opts] { limit?: number, start?: number }
226
244
  * @returns {Promise<object>}
227
245
  */
228
- export async function getTestPlan(cfg, xrayToken, issueId) {
246
+ export async function getTestPlan(cfg, xrayToken, issueId, opts = {}) {
247
+ const limit = opts.limit ?? 100;
248
+ const start = opts.start ?? 0;
229
249
  const data = await graphql(cfg, xrayToken, `
230
- query ($issueId: String!) {
250
+ query ($issueId: String!, $limit: Int!, $start: Int) {
231
251
  getTestPlan(issueId: $issueId) {
232
252
  issueId
233
253
  projectId
234
- tests(limit: 100) {
254
+ tests(limit: $limit, start: $start) {
235
255
  total
236
256
  results {
237
257
  issueId
@@ -246,7 +266,7 @@ export async function getTestPlan(cfg, xrayToken, issueId) {
246
266
  }
247
267
  }
248
268
  }
249
- `, { issueId: String(issueId) });
269
+ `, { issueId: String(issueId), limit, start });
250
270
  return data.getTestPlan;
251
271
  }
252
272
 
@@ -373,8 +393,58 @@ export async function getProjectSettings(cfg, xrayToken, projectId) {
373
393
  return data.getProjectSettings;
374
394
  }
375
395
 
376
- // ─── Xray GraphQLTest mutations ────────────────────────────────────────────
396
+ // ─── JIRA RESTIssue search ──────────────────────────────────────────────────
397
+
398
+ /**
399
+ * Search JIRA issues via JQL.
400
+ * @param {object} cfg
401
+ * @param {string} jql
402
+ * @param {string[]} [fields] Fields to return (default: summary, id, key)
403
+ * @param {number} [maxResults]
404
+ * @returns {Promise<object[]>} Array of issue objects
405
+ */
406
+ export async function searchIssues(cfg, jql, fields = ["summary", "id", "key"], maxResults = 10) {
407
+ const response = await axios.get(
408
+ `${cfg.jiraUrl}/rest/api/3/search`,
409
+ {
410
+ httpsAgent,
411
+ headers: jiraHeaders(cfg),
412
+ params: { jql, fields: fields.join(","), maxResults },
413
+ }
414
+ );
415
+ return response.data.issues || [];
416
+ }
417
+
418
+ // ─── Xray GraphQL — Test Set queries ──────────────────────────────────────────
377
419
 
420
+ /**
421
+ * Get Test Sets in a project (paginated).
422
+ * @param {object} cfg
423
+ * @param {string} xrayToken
424
+ * @param {object} [opts] { projectId, jql, limit, start }
425
+ * @returns {Promise<{total, results}>}
426
+ */
427
+ export async function getTestSets(cfg, xrayToken, opts = {}) {
428
+ const data = await graphql(cfg, xrayToken, `
429
+ query ($projectId: String, $jql: String, $limit: Int!, $start: Int) {
430
+ getTestSets(projectId: $projectId, jql: $jql, limit: $limit, start: $start) {
431
+ total
432
+ results {
433
+ issueId
434
+ projectId
435
+ }
436
+ }
437
+ }
438
+ `, {
439
+ projectId: opts.projectId || null,
440
+ jql: opts.jql || null,
441
+ limit: Math.min(opts.limit || 100, 100),
442
+ start: opts.start || 0,
443
+ });
444
+ return data.getTestSets;
445
+ }
446
+
447
+ // ─── Xray GraphQL — Test mutations ────────────────────────────────────────────
378
448
  /**
379
449
  * Set a test issue's type via Xray GraphQL.
380
450
  * @param {object} cfg
@@ -495,6 +565,31 @@ export async function updateTestFolder(cfg, xrayToken, issueId, folderPath) {
495
565
  `, { issueId: String(issueId), folderPath });
496
566
  }
497
567
 
568
+ // ─── Xray GraphQL — Precondition mutations ────────────────────────────────────
569
+
570
+ /**
571
+ * Link precondition issues to a test.
572
+ * @param {object} cfg
573
+ * @param {string} xrayToken
574
+ * @param {string} testIssueId Numeric Xray issue ID of the test
575
+ * @param {string[]} preconditionIssueIds Numeric Xray issue IDs of preconditions
576
+ * @returns {Promise<{addedPreconditions: string[], warnings: string[]}>}
577
+ */
578
+ export async function addPreconditionsToTest(cfg, xrayToken, testIssueId, preconditionIssueIds) {
579
+ const data = await graphql(cfg, xrayToken, `
580
+ mutation ($issueId: String!, $preconditionIssueIds: [String]!) {
581
+ addPreconditionsToTest(issueId: $issueId, preconditionIssueIds: $preconditionIssueIds) {
582
+ addedPreconditions
583
+ warnings
584
+ }
585
+ }
586
+ `, {
587
+ issueId: String(testIssueId),
588
+ preconditionIssueIds: preconditionIssueIds.map(String),
589
+ });
590
+ return data.addPreconditionsToTest;
591
+ }
592
+
498
593
  // ─── Xray GraphQL — Test Plan mutations ───────────────────────────────────────
499
594
 
500
595
  /**
@@ -978,21 +1073,20 @@ export async function getAttachment(cfg, xrayToken, attachmentId) {
978
1073
  export async function withRetry(fn, opts = {}) {
979
1074
  const maxRetries = opts.maxRetries ?? 5;
980
1075
  const baseDelay = opts.baseDelay ?? 2000;
981
- const retryOn = opts.retryOn ?? "issueId provided is not valid";
1076
+ // retryOn accepts a string or array of strings — retry if the error matches any
1077
+ const retryPatterns = Array.isArray(opts.retryOn)
1078
+ ? opts.retryOn
1079
+ : [opts.retryOn ?? "issueId provided is not valid"];
982
1080
 
983
1081
  let lastError;
984
1082
  for (let attempt = 0; attempt < maxRetries; attempt++) {
985
1083
  try {
986
- const delay = baseDelay * Math.pow(2, attempt);
987
- if (attempt > 0) {
988
- logger.wait(`Retry ${attempt}/${maxRetries - 1} after ${delay}ms...`);
989
- }
990
- await sleep(delay);
991
1084
  return await fn();
992
1085
  } catch (err) {
993
1086
  lastError = err;
994
1087
  const msg = err.message || "";
995
- if (!msg.includes(retryOn)) {
1088
+ const shouldRetry = retryPatterns.some((p) => msg.includes(p));
1089
+ if (!shouldRetry) {
996
1090
  if (msg.includes("disallowed to impersonate") || msg.includes("no valid active user exists")) {
997
1091
  throw new Error(
998
1092
  `Xray user authentication mismatch.\n\n` +
@@ -1002,6 +1096,11 @@ export async function withRetry(fn, opts = {}) {
1002
1096
  }
1003
1097
  throw err;
1004
1098
  }
1099
+ if (attempt < maxRetries - 1) {
1100
+ const delay = baseDelay * Math.pow(2, attempt);
1101
+ logger.wait(`Retry ${attempt + 1}/${maxRetries - 1} after ${delay}ms...`);
1102
+ await sleep(delay);
1103
+ }
1005
1104
  }
1006
1105
  }
1007
1106
  throw lastError;
@@ -1082,12 +1181,14 @@ export async function createTestExecution(cfg, xrayToken, opts) {
1082
1181
  environments = [],
1083
1182
  testIssueIds = [],
1084
1183
  testPlanKey,
1184
+ fixVersion,
1085
1185
  } = opts;
1086
1186
 
1087
1187
  // 1. Create JIRA issue
1088
1188
  const issue = await createIssue(cfg, "Test Execution", {
1089
1189
  summary: summary || `Test Execution — ${new Date().toLocaleString()}`,
1090
1190
  description: description || summary || "",
1191
+ ...(fixVersion ? { fixVersions: [fixVersion] } : {}),
1091
1192
  });
1092
1193
 
1093
1194
  logger.step(`Test Execution created: ${issue.key}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@msalaam/xray-qe-toolkit",
3
- "version": "1.5.0",
3
+ "version": "1.6.1",
4
4
  "description": "QE toolkit for Xray Cloud — test management, tests.json standardisation, Playwright result import, and CI pipeline integration.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -30,6 +30,7 @@
30
30
  "license": "UNLICENSED",
31
31
  "dependencies": {
32
32
  "@modelcontextprotocol/sdk": "^1.27.1",
33
+ "@msalaam/xray-qe-toolkit": "^1.6.0",
33
34
  "axios": "^1.13.4",
34
35
  "commander": "^13.1.0",
35
36
  "dotenv": "^17.2.3",
@@ -60,8 +60,27 @@
60
60
  "pattern": "^/"
61
61
  },
62
62
  "testSet": {
63
- "type": "string",
64
- "description": "Name of the Xray Test Set this test belongs to. Tests sharing the same value are grouped into a single Test Set issue in JIRA. xqt push creates the Test Set automatically if it doesn't exist and adds the test to it. Test Set → JIRA key mappings are stored in xray-mapping.json under _testSets."
63
+ "oneOf": [
64
+ {
65
+ "type": "string",
66
+ "description": "Name of a single Xray Test Set this test belongs to."
67
+ },
68
+ {
69
+ "type": "array",
70
+ "items": { "type": "string" },
71
+ "minItems": 1,
72
+ "description": "Names of multiple Xray Test Sets this test belongs to."
73
+ }
74
+ ],
75
+ "description": "Xray Test Set name(s) — string or array of strings. Tests are grouped into Test Set issues in JIRA. xqt push creates sets automatically and adds the test to each."
76
+ },
77
+ "preconditions": {
78
+ "type": "array",
79
+ "items": {
80
+ "type": "string",
81
+ "pattern": "^[A-Z][A-Z0-9_]+-[0-9]+$"
82
+ },
83
+ "description": "JIRA issue keys of Xray Precondition issues linked to this test (e.g. [\"PROJ-5\"]). xqt push calls addPreconditionsToTest on each push."
65
84
  },
66
85
  "requirementKeys": {
67
86
  "type": "array",
@@ -21,6 +21,7 @@
21
21
  10. [CI/CD Integration](#10-cicd-integration)
22
22
  11. [xray-mapping.json](#11-xray-mappingjson)
23
23
  12. [Configuration (.xrayrc)](#12-configuration-xrayrc)
24
+ 13. [Troubleshooting](#13-troubleshooting)
24
25
 
25
26
  > **This file covers the xqt toolkit commands and Xray configuration.**
26
27
  > For the full QE process — spec-driven workflow, greenfield/brownfield steps, AI agent
@@ -152,6 +153,7 @@ After running `npx xqt init`:
152
153
  "tags": ["smoke", "regression"],
153
154
  "folder": "/{{SERVICE_NAME}}/HealthCheck/Validation",
154
155
  "testSet": "Health Check",
156
+ "preconditions": ["APIEE-50"],
155
157
  "requirementKeys": [],
156
158
  "xray": {
157
159
  "summary": "Service returns 200 OK when healthy",
@@ -175,7 +177,8 @@ After running `npx xqt init`:
175
177
  | `skip` | boolean | — | `true` to exclude from push-tests |
176
178
  | `tags` | string[] | — | QE tags for categorisation and filtering |
177
179
  | `folder` | string | — | Xray repository folder path — must start with `/` |
178
- | `testSet` | string | — | Test Set name in Jira — groups tests by feature |
180
+ | `testSet` | string **or** string[] | — | Test Set name(s) in Jira — use a string or an array to assign to multiple sets |
181
+ | `preconditions` | string[] | — | JIRA keys of Xray Precondition issues to link (e.g. `["APIEE-50"]`) |
179
182
  | `requirementKeys` | string[] | — | JIRA issue keys this test covers (creates coverage links) |
180
183
  | `xray.summary` | string | Yes | Test case title (JIRA issue summary) |
181
184
  | `xray.description` | string | — | Detailed description |
@@ -213,14 +216,20 @@ npx xqt create-plan --summary "{{SERVICE_NAME}} v2 Regression"
213
216
  npx xqt create-plan --summary "Sprint 12 Smoke Tests" --version "2024.12"
214
217
  ```
215
218
 
216
- ### `xqt push-tests`
219
+ ### `xqt push-tests` (alias: `push`)
217
220
 
218
221
  Create or update tests in Xray Cloud, then sync the Test Plan membership and folder structure.
219
222
 
223
+ - New tests are **bulk-imported** via the Xray REST API when there are ≥ `bulkImportThreshold` (default: 50) to create — significantly faster for large suites. Force it at any count with `--bulk`
224
+ - Test Plan membership is **bi-directionally synced**: new tests are added and tests removed from `tests.json` are removed from the plan
225
+ - Test Sets are **deduplicated by name** — safe to run on a fresh clone, existing sets are found in JIRA before creating new ones
226
+ - `preconditions` are linked after every create or update
227
+
220
228
  ```bash
221
- npx xqt push-tests
222
- npx xqt push-tests --plan APIEE-1234 # override plan key
223
- npx xqt push-tests --verbose
229
+ npx xqt push
230
+ npx xqt push --plan APIEE-1234 # override plan key
231
+ npx xqt push --bulk # force bulk REST import regardless of test count
232
+ npx xqt push --verbose
224
233
  ```
225
234
 
226
235
  ### `xqt pull-tests`
@@ -232,15 +241,15 @@ npx xqt pull-tests --plan APIEE-1234
232
241
  npx xqt pull-tests --project APIEE --limit 200
233
242
  ```
234
243
 
235
- ### `xqt import-results`
244
+ ### `xqt import-results` (alias: `import`)
236
245
 
237
246
  Import test execution results into Xray. Creates a **new** Test Execution each time.
238
247
 
239
248
  ```bash
240
- npx xqt import-results --file test-results/results.json --env IOP-QA
241
- npx xqt import-results --file test-results/results.xml --env IOP-PROD
242
- npx xqt import-results --file test-results/results.json --env IOP-DEV \
243
- --plan APIEE-1234 --version "2024.12" --revision "a1b2c3d"
249
+ npx xqt import --file test-results/results.json --env IOP-QA
250
+ npx xqt import --file test-results/results.xml --env IOP-PROD
251
+ npx xqt import --file test-results/results.json --env IOP-DEV \
252
+ --plan APIEE-1234 --fix-version "2.5.0" --revision "a1b2c3d"
244
253
  ```
245
254
 
246
255
  **Options:**
@@ -250,10 +259,31 @@ npx xqt import-results --file test-results/results.json --env IOP-DEV \
250
259
  | `--file <path>` | Path to results file (`.json` = Playwright, `.xml` = JUnit) |
251
260
  | `--env <label>` | Environment label: `IOP-DEV`, `IOP-QA`, `IOP-PROD` |
252
261
  | `--plan <key>` | Test Plan key (overrides `.xrayrc`) |
253
- | `--version <ver>` | Fix version / release label |
254
- | `--revision <sha>` | Build number or git SHA |
262
+ | `--exec <key>` | Import INTO an existing execution (from `xqt exec`) |
263
+ | `--fix-version <ver>` | JIRA Fix Version name to stamp on the execution (e.g. `2.5.0`) |
264
+ | `--version <ver>` | Xray version label (free-form, separate from Fix Version) |
265
+ | `--revision <sha>` | Build number or git SHA (enables traceability to a specific commit) |
255
266
  | `--summary <text>` | Custom execution summary |
256
267
 
268
+ ### `xqt create-execution` (alias: `exec`)
269
+
270
+ Pre-create a Test Execution before running tests (for controlled test selection).
271
+
272
+ ```bash
273
+ EXEC_KEY=$(npx xqt exec --env IOP-QA --quiet)
274
+ npx playwright test
275
+ npx xqt import --file test-results/results.json --exec $EXEC_KEY
276
+ ```
277
+
278
+ | Flag | Description |
279
+ |---|---|
280
+ | `--env <label>` | Environment label |
281
+ | `--plan <key>` | Test Plan to link to |
282
+ | `--tests <ids>` | Comma-separated testIds or JIRA keys |
283
+ | `--summary <text>` | Custom execution title |
284
+ | `--fix-version <ver>` | JIRA Fix Version to stamp on the execution |
285
+ | `--quiet` | Print only the execution key |
286
+
257
287
  ### `xqt sync-folders`
258
288
 
259
289
  Sync the Xray repository folder structure from `folder` fields in tests.json.
@@ -306,7 +336,10 @@ npx xqt gen-pipeline --output .azure/ci.yml
306
336
 
307
337
  Tests live in **Test Sets** in Jira, grouped by feature (matching the feature name in `business-rules.yaml`).
308
338
 
309
- - Created automatically by `push-tests` from the `testSet` field in `tests.json`
339
+ - Assign a test to one set: `"testSet": "Health Check"`
340
+ - Assign to multiple sets: `"testSet": ["Health Check", "Smoke"]`
341
+ - Created automatically by `push` from the `testSet` field in `tests.json`
342
+ - **Deduplicated by name** — if `xray-mapping.json` is lost (e.g. fresh clone), sets are found by searching JIRA before creating a new one
310
343
  - Persistent across sprints — they represent what tests exist
311
344
  - Link Test Sets to Test Plans when entering a sprint
312
345
 
@@ -314,17 +347,20 @@ Tests live in **Test Sets** in Jira, grouped by feature (matching the feature na
314
347
 
315
348
  A Test Plan scopes testing for a specific sprint or release.
316
349
 
317
- - Create per sprint: `npx xqt create-plan --summary "Sprint 12 — {{SERVICE_NAME}}"`
350
+ - Create per sprint: `npx xqt plan --summary "Sprint 12 — {{SERVICE_NAME}}"`
318
351
  - Link relevant Test Sets to the Test Plan in Jira
319
352
  - The key is stored in `.xrayrc` (`testPlanKey`)
320
- - `import-results` links executions to the active plan
353
+ - `import` links executions to the active plan
354
+ - **`xqt push` performs a bi-directional sync** — new tests are added and tests removed from `tests.json` are removed from the plan
321
355
 
322
356
  ### Test Executions (ephemeral)
323
357
 
324
358
  A Test Execution represents one CI run.
325
- - Created automatically by `xqt import-results`
359
+ - Created automatically by `xqt import`
360
+ - **Pre-created** by `xqt exec` for controlled test selection (filter which tests run)
326
361
  - Tagged with the environment (`IOP-DEV`, `IOP-QA`, `IOP-PROD`)
327
362
  - Linked to the Test Plan
363
+ - Stamped with `--fix-version` and `--revision` for release traceability
328
364
 
329
365
  ### Summary
330
366
 
@@ -348,7 +384,7 @@ Set the default environment and allowed values in `.xrayrc`:
348
384
  Override per run with `--env`:
349
385
 
350
386
  ```bash
351
- npx xqt import-results --file results.json --env IOP-PROD
387
+ npx xqt import --file results.json --env IOP-PROD
352
388
  ```
353
389
 
354
390
  ---
@@ -387,12 +423,12 @@ The generated `playwright.config.ts` configures three reporters:
387
423
 
388
424
  ```bash
389
425
  # After running: npx playwright test
390
- npx xqt import-results --file test-results/results.json --env IOP-QA
426
+ npx xqt import --file test-results/results.json --env IOP-QA
391
427
  ```
392
428
 
393
429
  ### Test steps in Xray
394
430
 
395
- Steps defined with `test.step()` are automatically mapped to Xray step results:
431
+ Steps defined with `test.step()` are automatically mapped to Xray step results. **Screenshots captured on a failing step are attached directly to that step's evidence** in Xray — giving per-step failure screenshots in the Xray UI. Traces and other attachments are attached at the test level.
396
432
 
397
433
  ```typescript
398
434
  test('Create and retrieve user', async ({ request }) => {
@@ -481,9 +517,9 @@ Key import step (runs even on test failure):
481
517
  ```yaml
482
518
  - script: |
483
519
  REVISION=$(echo "$(Build.SourceVersion)" | cut -c1-8)
484
- npx xqt import-results \
520
+ npx xqt import \
485
521
  --file test-results/results.json \
486
- --version "$(Build.BuildNumber)" \
522
+ --fix-version "$(Build.BuildNumber)" \
487
523
  --revision "$REVISION"
488
524
  condition: succeededOrFailed()
489
525
  env:
@@ -551,11 +587,34 @@ Auto-managed by `push-tests` — do not edit manually.
551
587
  "defaultEnvironment": "IOP-DEV",
552
588
  "environments": ["IOP-DEV", "IOP-QA", "IOP-PROD"],
553
589
  "folderRoot": "/{{SERVICE_NAME}}",
554
- "xrayRegion": "us"
590
+ "xrayRegion": "us",
591
+ "bulkImportThreshold": 50,
592
+ "statusMapping": {
593
+ "interrupted": "ABORTED",
594
+ "skipped": "TODO"
595
+ }
555
596
  }
556
597
  ```
557
598
 
558
- ### xrayRegion values
599
+ | Field | Description |
600
+ |---|---|
601
+ | `testPlanKey` | Default Test Plan key |
602
+ | `defaultEnvironment` | Fallback environment when `--env` is not provided |
603
+ | `environments` | Allowed environment labels |
604
+ | `folderRoot` | Base folder path in Xray Test Repository |
605
+ | `xrayRegion` | `us` (default), `eu`, or `au` |
606
+ | `bulkImportThreshold` | Min new-test count to switch to bulk REST import (default: `50`) |
607
+ | `statusMapping` | Override default Playwright → Xray status mapping |
608
+
609
+ #### Playwright status mapping
610
+
611
+ By default: `passed→PASSED`, `failed→FAILED`, `timedOut→FAILED`, `interrupted→ABORTED`, `skipped→TODO`. Override any value:
612
+
613
+ ```json
614
+ { "statusMapping": { "interrupted": "FAILED", "skipped": "TODO" } }
615
+ ```
616
+
617
+ You can also set `XQT_BULK_THRESHOLD=<n>` as an environment variable to override the bulk threshold at runtime.
559
618
 
560
619
  | Value | GraphQL endpoint |
561
620
  |---|---|
@@ -567,4 +626,54 @@ You can also set `XRAY_REGION=eu` in `.env`.
567
626
 
568
627
  ---
569
628
 
629
+ ## 13. Troubleshooting
630
+
631
+ **Missing credentials**
632
+ ```
633
+ Error: Missing required config: xrayId, xraySecret
634
+ ```
635
+ Ensure `.env` exists and all required fields are populated. Copy from `.env.example`.
636
+
637
+ **Test not found in mapping after push**
638
+ ```bash
639
+ npx xqt status
640
+ npx xqt push --verbose
641
+ ```
642
+
643
+ **Test Set creation returns HTTP 400**
644
+ Ensure the **Test Set** issue type exists in your JIRA project and the user can create it.
645
+ > **Project Settings → Issue Types** — "Test Set" must appear in the list.
646
+
647
+ **Folder sync — permission error**
648
+ ```
649
+ User doesn't have permissions to view/edit test repository for project
650
+ ```
651
+ The `JIRA_EMAIL` user needs **Test Repository** write access in Xray.
652
+ > **Project Settings → Xray → Test Repository** → add your role or user.
653
+ Folder errors are non-fatal — tests are still created/updated correctly without folder assignment.
654
+
655
+ **`updateTestType` "not found" on newly created tests**
656
+ Xray Cloud is eventually consistent. `xqt push` retries automatically with backoff. If a test still fails, rerun `xqt push` — it will update the existing test rather than duplicate it.
657
+
658
+ **Too many tests for serial mode (slow push)**
659
+ ```bash
660
+ npx xqt push --bulk # force REST bulk import regardless of count
661
+ # or lower the threshold in .xrayrc:
662
+ { "bulkImportThreshold": 10 }
663
+ ```
664
+
665
+ **Import results — 0 tests matched**
666
+ Playwright tests must be annotated with an Xray key:
667
+ ```typescript
668
+ test.info().annotations.push({ type: 'xray', description: 'APIEE-1234' });
669
+ ```
670
+
671
+ **"disallowed to impersonate" error**
672
+ `JIRA_EMAIL` must match the email of the user who created the Xray API Key.
673
+
674
+ **EU / AU region**
675
+ Set `XRAY_REGION=eu` or `XRAY_REGION=au` in `.env`, or `"xrayRegion": "eu"` in `.xrayrc`.
676
+
677
+ ---
678
+
570
679
  *Generated by @msalaam/xray-qe-toolkit — See [npm package](https://www.npmjs.com/package/@msalaam/xray-qe-toolkit) for updates.*