@camunda/e2e-test-suite 0.0.1066 → 0.0.1068

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.
@@ -32,8 +32,7 @@ _8_10_1.test.describe('Cluster Setup Tests', () => {
32
32
  await clusterPage.assertClusterHealthyStatusWithRetry(clusterName, 120000, 1500000);
33
33
  }
34
34
  else {
35
- await (0, consoleApiHelpers_1.deleteClusterViaApi)(page, clusterName);
36
- const clusterUuid = await (0, consoleApiHelpers_1.createClusterViaApi)(page, clusterName);
35
+ const clusterUuid = await (0, consoleApiHelpers_1.ensureClusterViaApi)(page, clusterName);
37
36
  await (0, consoleApiHelpers_1.waitForClusterHealthyViaApi)(page, clusterUuid);
38
37
  await homePage.clickClusters();
39
38
  }
@@ -63,8 +62,7 @@ _8_10_1.test.describe('Cluster Setup Tests', () => {
63
62
  const apiClientName = 'Test_API_Client' + (await (0, _setup_1.generateRandomStringAsync)(3));
64
63
  const mcpClientName = 'MCP_Test_API_Client' + (await (0, _setup_1.generateRandomStringAsync)(3));
65
64
  await (0, UtilitiesPage_1.loginWithRetry)(page, loginPage, users_1.testUsers.mainUser, (testInfo.workerIndex + 1) * 1000);
66
- await (0, consoleApiHelpers_1.deleteClusterViaApi)(page, clusterName);
67
- const clusterUuid = await (0, consoleApiHelpers_1.createClusterViaApi)(page, clusterName);
65
+ const clusterUuid = await (0, consoleApiHelpers_1.ensureClusterViaApi)(page, clusterName);
68
66
  await (0, consoleApiHelpers_1.waitForClusterHealthyViaApi)(page, clusterUuid, 1500000);
69
67
  await homePage.clickClusters();
70
68
  await clusterPage.clickClusterLink(clusterName);
@@ -99,8 +97,7 @@ _8_10_1.test.describe('Cluster Setup Tests', () => {
99
97
  await clusterPage.assertClusterHealthyStatusWithRetry(clusterName, 120000, 1500000);
100
98
  }
101
99
  else {
102
- await (0, consoleApiHelpers_1.deleteClusterViaApi)(page, clusterName);
103
- const clusterUuid = await (0, consoleApiHelpers_1.createClusterViaApi)(page, clusterName, 'AWS');
100
+ const clusterUuid = await (0, consoleApiHelpers_1.ensureClusterViaApi)(page, clusterName, 'AWS');
104
101
  await (0, consoleApiHelpers_1.waitForClusterHealthyViaApi)(page, clusterUuid, 1500000);
105
102
  await homePage.clickClusters();
106
103
  }
@@ -64,18 +64,42 @@ async function getUserToken(page) {
64
64
  const TRANSIENT_API_STATUSES = [429, 500, 502, 503, 504];
65
65
  const API_MAX_ATTEMPTS = 5;
66
66
  // Overrides the global actionTimeout (10s) for Console API calls only: cluster
67
- // creation regularly needs longer than that to answer.
68
- const API_TIMEOUT_MS = 15000;
67
+ // listings answer in tens of milliseconds, so 15s is generous for a read.
68
+ const API_READ_TIMEOUT_MS = 15000;
69
+ // Cluster mutations are an order of magnitude slower than reads on Console:
70
+ // POST /clusters was measured at 8-12s and DELETE at 7-9s against INT. A 15s
71
+ // ceiling sat inside that band, so the client aborted requests the server went
72
+ // on to complete — leaving a cluster nobody was tracking.
73
+ const API_WRITE_TIMEOUT_MS = 45000;
74
+ // POST is the only non-idempotent verb used here: repeating it creates a second
75
+ // cluster under the same name. Let its timeout surface instead, so the caller
76
+ // can recover by looking the cluster up by name (see createClusterViaApi).
77
+ const TIMEOUT_RETRYABLE_METHODS = ['GET', 'PATCH', 'DELETE'];
69
78
  async function clusterApiRequest(page, method, url, data) {
70
79
  let response;
71
80
  for (let attempt = 1; attempt <= API_MAX_ATTEMPTS; attempt++) {
72
81
  const token = await getUserToken(page);
73
- response = await page.request.fetch(url, {
74
- method,
75
- headers: { Authorization: `Bearer ${token}` },
76
- timeout: API_TIMEOUT_MS,
77
- ...(data === undefined ? {} : { data }),
78
- });
82
+ try {
83
+ response = await page.request.fetch(url, {
84
+ method,
85
+ headers: { Authorization: `Bearer ${token}` },
86
+ timeout: method === 'GET' ? API_READ_TIMEOUT_MS : API_WRITE_TIMEOUT_MS,
87
+ ...(data === undefined ? {} : { data }),
88
+ });
89
+ }
90
+ catch (error) {
91
+ // A timeout or transport error throws rather than returning a status, so
92
+ // the status-based retry below never covered it and API_MAX_ATTEMPTS did
93
+ // not apply to the one failure mode that actually happens.
94
+ console.log(`Console API ${method} ${url} threw ${error.message} ` +
95
+ `(attempt ${attempt}/${API_MAX_ATTEMPTS})`);
96
+ if (!TIMEOUT_RETRYABLE_METHODS.includes(method) ||
97
+ attempt === API_MAX_ATTEMPTS) {
98
+ throw error;
99
+ }
100
+ await (0, sleep_1.sleep)(attempt * 10000);
101
+ continue;
102
+ }
79
103
  if (!TRANSIENT_API_STATUSES.includes(response.status()))
80
104
  return response;
81
105
  console.log(`Console API ${method} ${url} returned HTTP ${response.status()} ` +
@@ -113,8 +137,22 @@ async function findClusterByName(page, clusterName) {
113
137
  : data.clusters ?? [];
114
138
  return clusters.find((c) => c.name === clusterName);
115
139
  }
140
+ // A cluster is not always listable the instant its create lands, so poll a few
141
+ // times before concluding a timed-out create really did not happen.
142
+ const CLUSTER_RECOVERY_ATTEMPTS = 6;
143
+ const CLUSTER_RECOVERY_INTERVAL_MS = 5000;
144
+ async function findClusterByNameWithRetry(page, clusterName) {
145
+ for (let attempt = 1; attempt <= CLUSTER_RECOVERY_ATTEMPTS; attempt++) {
146
+ const cluster = await findClusterByName(page, clusterName);
147
+ if (cluster)
148
+ return cluster;
149
+ if (attempt < CLUSTER_RECOVERY_ATTEMPTS) {
150
+ await (0, sleep_1.sleep)(CLUSTER_RECOVERY_INTERVAL_MS);
151
+ }
152
+ }
153
+ return undefined;
154
+ }
116
155
  async function deleteClusterViaApi(page, clusterName) {
117
- const token = await getUserToken(page);
118
156
  const orgId = getOrgId();
119
157
  const deadline = Date.now() + 180000;
120
158
  // Re-deleting a cluster that is already terminating can answer 409/400.
@@ -127,8 +165,14 @@ async function deleteClusterViaApi(page, clusterName) {
127
165
  if (!cluster)
128
166
  return;
129
167
  if (!deleteIssued.has(cluster.uuid)) {
130
- const response = await page.request.delete(`${getConsoleBaseUrl()}/api/orgs/${orgId}/clusters/${cluster.uuid}`, { headers: { Authorization: `Bearer ${token}` } });
131
- if (!response.ok() && response.status() !== 404) {
168
+ // Routed through clusterApiRequest for the write timeout and retries: a
169
+ // bare page.request.delete inherits the 10s actionTimeout, which DELETE
170
+ // /clusters (7-9s at INT) can exceed.
171
+ const response = await clusterApiRequest(page, 'DELETE', `${getConsoleBaseUrl()}/api/orgs/${orgId}/clusters/${cluster.uuid}`);
172
+ // 409 is the retry landing on a cluster the first attempt already put
173
+ // into termination -- the outcome this asked for, not a failure.
174
+ const alreadyTerminating = [404, 409].includes(response.status());
175
+ if (!response.ok() && !alreadyTerminating) {
132
176
  throw new Error(`Failed to delete cluster "${clusterName}" (${cluster.uuid}): ` +
133
177
  `HTTP ${response.status()} - ${await response.text()}`);
134
178
  }
@@ -315,23 +359,40 @@ async function createClusterViaApi(page, clusterName, region = 'GCP', tasklistV2
315
359
  generationId = resolveGenerationId();
316
360
  }
317
361
  const k8sContextId = region.toUpperCase() === 'AWS' ? AWS_REGION_ID : GCP_REGION_ID;
318
- const createResponse = await clusterApiRequest(page, 'POST', `${getConsoleBaseUrl()}/api/orgs/${orgId}/clusters`, {
319
- name: clusterName,
320
- planTypeId: PLAN_TYPE_ID,
321
- channelId,
322
- generationId,
323
- k8sContextId,
324
- autoUpdate: true,
325
- stageLabel: 'dev',
326
- // Always sent explicitly rather than omitted: Console defaults an unset
327
- // field to v2-enabled for any generation >= 8.8, and the switch to v2 is
328
- // one-way -- a cluster that ends up v2-enabled can never be brought back
329
- // to v1 for the rest of its life. v2 cannot run job-worker-based user
330
- // tasks, draft variables, or public start forms, so the v1 leg
331
- // (@tasklistV1-tagged suites) depends on this being false, and the v2 leg
332
- // equally depends on it being true.
333
- tasklistV2Enabled,
334
- });
362
+ let createResponse;
363
+ try {
364
+ createResponse = await clusterApiRequest(page, 'POST', `${getConsoleBaseUrl()}/api/orgs/${orgId}/clusters`, {
365
+ name: clusterName,
366
+ planTypeId: PLAN_TYPE_ID,
367
+ channelId,
368
+ generationId,
369
+ k8sContextId,
370
+ autoUpdate: true,
371
+ stageLabel: 'dev',
372
+ // Always sent explicitly rather than omitted: Console defaults an unset
373
+ // field to v2-enabled for any generation >= 8.8, and the switch to v2 is
374
+ // one-way -- a cluster that ends up v2-enabled can never be brought back
375
+ // to v1 for the rest of its life. v2 cannot run job-worker-based user
376
+ // tasks, draft variables, or public start forms, so the v1 leg
377
+ // (@tasklistV1-tagged suites) depends on this being false, and the v2 leg
378
+ // equally depends on it being true.
379
+ tasklistV2Enabled,
380
+ });
381
+ }
382
+ catch (error) {
383
+ // Console does not cancel the create when the client stops waiting: it
384
+ // answers /clusters slower than the request timeout while still creating
385
+ // the cluster. Adopt what the timed-out POST made instead of re-POSTing,
386
+ // which would leave two clusters under the same name and break
387
+ // clickClusterLink in strict mode.
388
+ const recovered = await findClusterByNameWithRetry(page, clusterName);
389
+ if (!recovered)
390
+ throw error;
391
+ console.log(`Cluster "${clusterName}" create timed out but the cluster exists: ` +
392
+ `clusterId=${recovered.uuid} namespace=${recovered.uuid}-zeebe ` +
393
+ `orgId=${orgId} tasklistMode=${tasklistV2Enabled ? 'v2' : 'v1'}`);
394
+ return recovered.uuid;
395
+ }
335
396
  if (createResponse.status() !== 200 && createResponse.status() !== 201) {
336
397
  throw new Error(`Failed to create cluster "${clusterName}": ` +
337
398
  `HTTP ${createResponse.status()} - ${await createResponse.text()}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camunda/e2e-test-suite",
3
- "version": "0.0.1066",
3
+ "version": "0.0.1068",
4
4
  "description": "End-to-end test helpers for Camunda 8",
5
5
  "repository": {
6
6
  "type": "git",