@matware/e2e-runner 1.2.1 → 1.3.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.
Files changed (88) hide show
  1. package/.claude-plugin/marketplace.json +52 -0
  2. package/.claude-plugin/plugin.json +17 -3
  3. package/.mcp.json +2 -2
  4. package/.opencode/commands/create-test.md +63 -0
  5. package/.opencode/commands/run.md +50 -0
  6. package/.opencode/commands/verify-issue.md +62 -0
  7. package/.opencode/skills/e2e-testing/SKILL.md +181 -0
  8. package/.opencode/skills/e2e-testing/references/action-types.md +143 -0
  9. package/.opencode/skills/e2e-testing/references/auth-strategies.md +91 -0
  10. package/.opencode/skills/e2e-testing/references/graphql.md +59 -0
  11. package/.opencode/skills/e2e-testing/references/issue-verification.md +59 -0
  12. package/.opencode/skills/e2e-testing/references/multi-pool.md +60 -0
  13. package/.opencode/skills/e2e-testing/references/network-debugging.md +62 -0
  14. package/.opencode/skills/e2e-testing/references/test-json-format.md +163 -0
  15. package/.opencode/skills/e2e-testing/references/troubleshooting.md +224 -0
  16. package/.opencode/skills/e2e-testing/references/variables.md +41 -0
  17. package/.opencode/skills/e2e-testing/references/visual-verification.md +89 -0
  18. package/LICENSE +190 -0
  19. package/OPENCODE.md +166 -0
  20. package/README.md +165 -104
  21. package/agents/test-creator.md +54 -1
  22. package/agents/test-improver.md +37 -0
  23. package/bin/cli.js +409 -16
  24. package/commands/capture.md +45 -0
  25. package/commands/create-test.md +16 -1
  26. package/opencode.json +11 -0
  27. package/package.json +7 -2
  28. package/scripts/setup-opencode.sh +113 -0
  29. package/skills/e2e-testing/SKILL.md +10 -3
  30. package/skills/e2e-testing/references/action-types.md +48 -5
  31. package/skills/e2e-testing/references/auth-strategies.md +91 -0
  32. package/skills/e2e-testing/references/graphql.md +59 -0
  33. package/skills/e2e-testing/references/issue-verification.md +59 -0
  34. package/skills/e2e-testing/references/multi-pool.md +60 -0
  35. package/skills/e2e-testing/references/network-debugging.md +62 -0
  36. package/skills/e2e-testing/references/test-json-format.md +4 -0
  37. package/skills/e2e-testing/references/troubleshooting.md +44 -2
  38. package/skills/e2e-testing/references/variables.md +41 -0
  39. package/skills/e2e-testing/references/visual-verification.md +89 -0
  40. package/src/actions.js +475 -2
  41. package/src/ai-generate.js +139 -8
  42. package/src/app-pool.js +339 -0
  43. package/src/config.js +266 -5
  44. package/src/dashboard.js +216 -17
  45. package/src/db.js +191 -7
  46. package/src/index.js +12 -9
  47. package/src/learner-sqlite.js +458 -0
  48. package/src/learner.js +78 -6
  49. package/src/mcp-tools.js +1348 -51
  50. package/src/module-resolver.js +37 -0
  51. package/src/narrate.js +65 -0
  52. package/src/pool-manager.js +229 -0
  53. package/src/pool.js +301 -31
  54. package/src/reporter.js +86 -2
  55. package/src/runner.js +480 -71
  56. package/src/sync/auth.js +354 -0
  57. package/src/sync/client.js +572 -0
  58. package/src/sync/hub-routes.js +816 -0
  59. package/src/sync/index.js +68 -0
  60. package/src/sync/middleware.js +347 -0
  61. package/src/sync/queue.js +209 -0
  62. package/src/sync/schema.js +540 -0
  63. package/src/verify.js +10 -7
  64. package/src/visual-diff.js +446 -0
  65. package/src/watch.js +384 -0
  66. package/templates/build-dashboard.js +47 -6
  67. package/templates/dashboard/js/api.js +62 -0
  68. package/templates/dashboard/js/init.js +13 -0
  69. package/templates/dashboard/js/keyboard.js +46 -0
  70. package/templates/dashboard/js/state.js +40 -0
  71. package/templates/dashboard/js/toast.js +41 -0
  72. package/templates/dashboard/js/utils.js +216 -0
  73. package/templates/dashboard/js/view-live.js +181 -0
  74. package/templates/dashboard/js/view-runs.js +676 -0
  75. package/templates/dashboard/js/view-tests.js +294 -0
  76. package/templates/dashboard/js/view-watch.js +242 -0
  77. package/templates/dashboard/js/websocket.js +116 -0
  78. package/templates/dashboard/styles/base.css +69 -0
  79. package/templates/dashboard/styles/components.css +117 -0
  80. package/templates/dashboard/styles/view-live.css +97 -0
  81. package/templates/dashboard/styles/view-runs.css +243 -0
  82. package/templates/dashboard/styles/view-tests.css +96 -0
  83. package/templates/dashboard/styles/view-watch.css +53 -0
  84. package/templates/dashboard/template.html +181 -100
  85. package/templates/dashboard.html +1614 -547
  86. package/templates/sample-test.json +0 -8
  87. package/templates/dashboard/app.js +0 -1152
  88. package/templates/dashboard/styles.css +0 -413
package/src/config.js CHANGED
@@ -12,6 +12,22 @@ import fs from 'fs';
12
12
  import path from 'path';
13
13
  import { pathToFileURL } from 'url';
14
14
 
15
+ /** Deep merge utility for nested config objects */
16
+ function deepMerge(...objects) {
17
+ const result = {};
18
+ for (const obj of objects) {
19
+ if (!obj || typeof obj !== 'object') continue;
20
+ for (const key of Object.keys(obj)) {
21
+ if (obj[key] && typeof obj[key] === 'object' && !Array.isArray(obj[key])) {
22
+ result[key] = deepMerge(result[key] || {}, obj[key]);
23
+ } else if (obj[key] !== undefined) {
24
+ result[key] = obj[key];
25
+ }
26
+ }
27
+ }
28
+ return result;
29
+ }
30
+
15
31
  const DEFAULTS = {
16
32
  baseUrl: 'http://host.docker.internal:3000',
17
33
  poolUrl: 'ws://localhost:3333',
@@ -24,6 +40,7 @@ const DEFAULTS = {
24
40
  connectRetries: 3,
25
41
  connectRetryDelay: 2000,
26
42
  poolPort: 3333,
43
+ poolDriver: 'auto',
27
44
  maxSessions: 10,
28
45
  retries: 0,
29
46
  retryDelay: 1000,
@@ -38,6 +55,11 @@ const DEFAULTS = {
38
55
  failOnNetworkError: false,
39
56
  actionRetries: 0,
40
57
  actionRetryDelay: 500,
58
+ screencast: false,
59
+ screencastQuality: 60,
60
+ screencastMaxWidth: 800,
61
+ screencastMaxHeight: 600,
62
+ screencastEveryNthFrame: 1,
41
63
  anthropicApiKey: null,
42
64
  anthropicModel: 'claude-sonnet-4-5-20250929',
43
65
  authToken: null,
@@ -51,12 +73,86 @@ const DEFAULTS = {
51
73
  neo4jPassword: 'e2erunner',
52
74
  neo4jBoltPort: 7687,
53
75
  neo4jHttpPort: 7474,
76
+ verificationStrictness: 'moderate',
77
+ verificationThreshold: 0.02,
78
+ goldenDir: null,
79
+ networkIgnoreDomains: [],
80
+ // App pool: isolated app environments per test
81
+ appPool: {
82
+ enabled: false,
83
+ driver: 'docker', // 'docker' | 'zeroboot'
84
+ image: null, // Docker image to run (docker driver)
85
+ containerPort: 3000, // Port the app listens on inside the container
86
+ cmd: null, // Optional command override
87
+ envVars: null, // { KEY: 'value' } for the container
88
+ forkBasePort: 4000, // Host port range start for forked instances
89
+ forkHost: 'localhost', // Host for health checks from runner
90
+ forkProtocol: 'http',
91
+ maxForks: 10, // Max concurrent forks
92
+ readyCheck: null, // Path to poll for readiness (e.g. '/health')
93
+ readyTimeout: 15000, // ms to wait for fork to be ready
94
+ zeroboot: { // Zeroboot-specific config
95
+ apiUrl: 'http://localhost:8484',
96
+ templateId: null, // Pre-created template ID
97
+ },
98
+ },
99
+ authLoginEndpoint: null,
100
+ authCredentials: null,
101
+ authTokenPath: 'token',
102
+ gqlEndpoint: '/api/graphql',
103
+ gqlAuthHeader: 'Authorization',
104
+ gqlAuthKey: 'accessToken',
105
+ gqlAuthPrefix: 'Bearer ',
106
+ poolUrls: null,
107
+ watchInterval: null,
108
+ watchRunOnStart: true,
109
+ watchGitPoll: false,
110
+ watchGitBranch: null,
111
+ watchGitInterval: '30s',
112
+ watchWebhookUrl: null,
113
+ watchWebhookEvents: 'failure',
114
+ watchProjects: null,
115
+
116
+ // Sync configuration
117
+ sync: {
118
+ mode: 'standalone', // 'standalone' | 'hub' | 'agent'
119
+ hub: {
120
+ port: null, // null = use dashboardPort
121
+ tls: {
122
+ enabled: false,
123
+ certPath: null,
124
+ keyPath: null,
125
+ mtls: false,
126
+ caPath: null,
127
+ },
128
+ allowRegistration: true,
129
+ requireApproval: false,
130
+ masterKeyEnv: 'E2E_SYNC_MASTER_KEY',
131
+ },
132
+ agent: {
133
+ hubUrl: null,
134
+ instanceId: null,
135
+ displayName: null,
136
+ apiKeyEnv: 'E2E_SYNC_API_KEY',
137
+ totpSecretEnv: 'E2E_SYNC_TOTP',
138
+ tls: {
139
+ certPath: null,
140
+ keyPath: null,
141
+ caPath: null,
142
+ },
143
+ autoSync: true,
144
+ pullOnDashboard: true,
145
+ offlineQueue: true,
146
+ queueRetryInterval: 60,
147
+ },
148
+ },
54
149
  };
55
150
 
56
151
  function loadEnvVars() {
57
152
  const env = {};
58
153
  if (process.env.BASE_URL) env.baseUrl = process.env.BASE_URL;
59
154
  if (process.env.CHROME_POOL_URL) env.poolUrl = process.env.CHROME_POOL_URL;
155
+ if (process.env.CHROME_POOL_URLS) env.poolUrls = process.env.CHROME_POOL_URLS.split(',').map(u => u.trim()).filter(Boolean);
60
156
  if (process.env.TESTS_DIR) env.testsDir = process.env.TESTS_DIR;
61
157
  if (process.env.MODULES_DIR) env.modulesDir = process.env.MODULES_DIR;
62
158
  if (process.env.SCREENSHOTS_DIR) env.screenshotsDir = process.env.SCREENSHOTS_DIR;
@@ -73,6 +169,12 @@ function loadEnvVars() {
73
169
  if (process.env.FAIL_ON_NETWORK_ERROR) env.failOnNetworkError = process.env.FAIL_ON_NETWORK_ERROR === 'true' || process.env.FAIL_ON_NETWORK_ERROR === '1';
74
170
  if (process.env.ACTION_RETRIES) env.actionRetries = parseInt(process.env.ACTION_RETRIES);
75
171
  if (process.env.ACTION_RETRY_DELAY) env.actionRetryDelay = parseInt(process.env.ACTION_RETRY_DELAY);
172
+ if (process.env.POOL_DRIVER) env.poolDriver = process.env.POOL_DRIVER;
173
+ if (process.env.SCREENCAST) env.screencast = process.env.SCREENCAST === 'true' || process.env.SCREENCAST === '1';
174
+ if (process.env.SCREENCAST_QUALITY) env.screencastQuality = parseInt(process.env.SCREENCAST_QUALITY);
175
+ if (process.env.SCREENCAST_MAX_WIDTH) env.screencastMaxWidth = parseInt(process.env.SCREENCAST_MAX_WIDTH);
176
+ if (process.env.SCREENCAST_MAX_HEIGHT) env.screencastMaxHeight = parseInt(process.env.SCREENCAST_MAX_HEIGHT);
177
+ if (process.env.SCREENCAST_EVERY_NTH_FRAME) env.screencastEveryNthFrame = parseInt(process.env.SCREENCAST_EVERY_NTH_FRAME);
76
178
  if (process.env.ANTHROPIC_API_KEY) env.anthropicApiKey = process.env.ANTHROPIC_API_KEY;
77
179
  if (process.env.ANTHROPIC_MODEL) env.anthropicModel = process.env.ANTHROPIC_MODEL;
78
180
  if (process.env.AUTH_TOKEN) env.authToken = process.env.AUTH_TOKEN;
@@ -86,6 +188,126 @@ function loadEnvVars() {
86
188
  if (process.env.NEO4J_PASSWORD) env.neo4jPassword = process.env.NEO4J_PASSWORD;
87
189
  if (process.env.NEO4J_BOLT_PORT) env.neo4jBoltPort = parseInt(process.env.NEO4J_BOLT_PORT);
88
190
  if (process.env.NEO4J_HTTP_PORT) env.neo4jHttpPort = parseInt(process.env.NEO4J_HTTP_PORT);
191
+ if (process.env.NETWORK_IGNORE_DOMAINS) env.networkIgnoreDomains = process.env.NETWORK_IGNORE_DOMAINS.split(',').map(d => d.trim()).filter(Boolean);
192
+ if (process.env.AUTH_LOGIN_ENDPOINT) env.authLoginEndpoint = process.env.AUTH_LOGIN_ENDPOINT;
193
+ if (process.env.AUTH_TOKEN_PATH) env.authTokenPath = process.env.AUTH_TOKEN_PATH;
194
+ // credentials.env convention: E2E_USERNAME + E2E_PASSWORD → authCredentials
195
+ // Sends both email and username fields so the API accepts whichever it expects.
196
+ // E2E_AUTH_FIELD overrides to send a single field if desired.
197
+ if (process.env.E2E_USERNAME && process.env.E2E_PASSWORD) {
198
+ if (process.env.E2E_AUTH_FIELD) {
199
+ env.authCredentials = {
200
+ [process.env.E2E_AUTH_FIELD]: process.env.E2E_USERNAME,
201
+ password: process.env.E2E_PASSWORD,
202
+ };
203
+ } else {
204
+ env.authCredentials = {
205
+ email: process.env.E2E_USERNAME,
206
+ username: process.env.E2E_USERNAME,
207
+ password: process.env.E2E_PASSWORD,
208
+ };
209
+ }
210
+ }
211
+ if (process.env.E2E_LOGIN_ENDPOINT) env.authLoginEndpoint = process.env.E2E_LOGIN_ENDPOINT;
212
+ if (process.env.E2E_TOKEN_PATH) env.authTokenPath = process.env.E2E_TOKEN_PATH;
213
+ if (process.env.GQL_ENDPOINT) env.gqlEndpoint = process.env.GQL_ENDPOINT;
214
+ if (process.env.GQL_AUTH_HEADER) env.gqlAuthHeader = process.env.GQL_AUTH_HEADER;
215
+ if (process.env.GQL_AUTH_KEY) env.gqlAuthKey = process.env.GQL_AUTH_KEY;
216
+ if (process.env.GQL_AUTH_PREFIX) env.gqlAuthPrefix = process.env.GQL_AUTH_PREFIX;
217
+ if (process.env.WATCH_INTERVAL) env.watchInterval = process.env.WATCH_INTERVAL;
218
+ if (process.env.WATCH_WEBHOOK_URL) env.watchWebhookUrl = process.env.WATCH_WEBHOOK_URL;
219
+ if (process.env.WATCH_WEBHOOK_EVENTS) env.watchWebhookEvents = process.env.WATCH_WEBHOOK_EVENTS;
220
+ if (process.env.WATCH_GIT_POLL) env.watchGitPoll = process.env.WATCH_GIT_POLL === 'true' || process.env.WATCH_GIT_POLL === '1';
221
+ if (process.env.WATCH_GIT_BRANCH) env.watchGitBranch = process.env.WATCH_GIT_BRANCH;
222
+ if (process.env.WATCH_GIT_INTERVAL) env.watchGitInterval = process.env.WATCH_GIT_INTERVAL;
223
+ if (process.env.VERIFICATION_STRICTNESS) {
224
+ const val = process.env.VERIFICATION_STRICTNESS.toLowerCase();
225
+ if (['strict', 'moderate', 'lenient'].includes(val)) {
226
+ env.verificationStrictness = val;
227
+ }
228
+ }
229
+ if (process.env.VERIFICATION_THRESHOLD) env.verificationThreshold = parseFloat(process.env.VERIFICATION_THRESHOLD);
230
+ if (process.env.GOLDEN_DIR) env.goldenDir = process.env.GOLDEN_DIR;
231
+
232
+ // App pool configuration from env vars
233
+ if (process.env.APP_POOL_ENABLED) {
234
+ env.appPool = env.appPool || {};
235
+ env.appPool.enabled = process.env.APP_POOL_ENABLED === 'true' || process.env.APP_POOL_ENABLED === '1';
236
+ }
237
+ if (process.env.APP_POOL_DRIVER) {
238
+ env.appPool = env.appPool || {};
239
+ env.appPool.driver = process.env.APP_POOL_DRIVER;
240
+ }
241
+ if (process.env.APP_POOL_IMAGE) {
242
+ env.appPool = env.appPool || {};
243
+ env.appPool.image = process.env.APP_POOL_IMAGE;
244
+ }
245
+ if (process.env.APP_POOL_CONTAINER_PORT) {
246
+ env.appPool = env.appPool || {};
247
+ env.appPool.containerPort = parseInt(process.env.APP_POOL_CONTAINER_PORT);
248
+ }
249
+ if (process.env.APP_POOL_BASE_PORT) {
250
+ env.appPool = env.appPool || {};
251
+ env.appPool.forkBasePort = parseInt(process.env.APP_POOL_BASE_PORT);
252
+ }
253
+ if (process.env.APP_POOL_MAX_FORKS) {
254
+ env.appPool = env.appPool || {};
255
+ env.appPool.maxForks = parseInt(process.env.APP_POOL_MAX_FORKS);
256
+ }
257
+ if (process.env.APP_POOL_READY_CHECK) {
258
+ env.appPool = env.appPool || {};
259
+ env.appPool.readyCheck = process.env.APP_POOL_READY_CHECK;
260
+ }
261
+ if (process.env.APP_POOL_READY_TIMEOUT) {
262
+ env.appPool = env.appPool || {};
263
+ env.appPool.readyTimeout = parseInt(process.env.APP_POOL_READY_TIMEOUT);
264
+ }
265
+ if (process.env.ZEROBOOT_API_URL) {
266
+ env.appPool = env.appPool || {};
267
+ env.appPool.zeroboot = env.appPool.zeroboot || {};
268
+ env.appPool.zeroboot.apiUrl = process.env.ZEROBOOT_API_URL;
269
+ }
270
+ if (process.env.ZEROBOOT_TEMPLATE_ID) {
271
+ env.appPool = env.appPool || {};
272
+ env.appPool.zeroboot = env.appPool.zeroboot || {};
273
+ env.appPool.zeroboot.templateId = process.env.ZEROBOOT_TEMPLATE_ID;
274
+ }
275
+
276
+ // Sync configuration from env vars
277
+ if (process.env.E2E_SYNC_MODE) {
278
+ const mode = process.env.E2E_SYNC_MODE.toLowerCase();
279
+ if (['standalone', 'hub', 'agent'].includes(mode)) {
280
+ env.sync = env.sync || {};
281
+ env.sync.mode = mode;
282
+ }
283
+ }
284
+ if (process.env.E2E_SYNC_HUB_URL) {
285
+ env.sync = env.sync || {};
286
+ env.sync.agent = env.sync.agent || {};
287
+ env.sync.agent.hubUrl = process.env.E2E_SYNC_HUB_URL;
288
+ }
289
+ if (process.env.E2E_SYNC_INSTANCE_ID) {
290
+ env.sync = env.sync || {};
291
+ env.sync.agent = env.sync.agent || {};
292
+ env.sync.agent.instanceId = process.env.E2E_SYNC_INSTANCE_ID;
293
+ }
294
+ if (process.env.E2E_SYNC_DISPLAY_NAME) {
295
+ env.sync = env.sync || {};
296
+ env.sync.agent = env.sync.agent || {};
297
+ env.sync.agent.displayName = process.env.E2E_SYNC_DISPLAY_NAME;
298
+ }
299
+ if (process.env.E2E_SYNC_HUB_PORT) {
300
+ env.sync = env.sync || {};
301
+ env.sync.hub = env.sync.hub || {};
302
+ env.sync.hub.port = parseInt(process.env.E2E_SYNC_HUB_PORT);
303
+ }
304
+ if (process.env.E2E_SYNC_TLS_ENABLED) {
305
+ env.sync = env.sync || {};
306
+ env.sync.hub = env.sync.hub || {};
307
+ env.sync.hub.tls = env.sync.hub.tls || {};
308
+ env.sync.hub.tls.enabled = process.env.E2E_SYNC_TLS_ENABLED === 'true' || process.env.E2E_SYNC_TLS_ENABLED === '1';
309
+ }
310
+
89
311
  return env;
90
312
  }
91
313
 
@@ -107,11 +329,10 @@ async function loadConfigFile(cwd) {
107
329
  return {};
108
330
  }
109
331
 
110
- /** Load .env file from cwd into process.env (no deps, KEY=VALUE format). */
111
- function loadDotEnv(cwd) {
112
- const envPath = path.join(cwd, '.env');
113
- if (!fs.existsSync(envPath)) return;
114
- const lines = fs.readFileSync(envPath, 'utf-8').split('\n');
332
+ /** Load a KEY=VALUE file into process.env (no deps). */
333
+ function loadEnvFile(filePath) {
334
+ if (!fs.existsSync(filePath)) return;
335
+ const lines = fs.readFileSync(filePath, 'utf-8').split('\n');
115
336
  for (const line of lines) {
116
337
  const trimmed = line.trim();
117
338
  if (!trimmed || trimmed.startsWith('#')) continue;
@@ -130,6 +351,14 @@ function loadDotEnv(cwd) {
130
351
  }
131
352
  }
132
353
 
354
+ /** Load .env and credentials.env from cwd into process.env. */
355
+ function loadDotEnv(cwd) {
356
+ loadEnvFile(path.join(cwd, '.env'));
357
+ // credentials.env — search e2e/ subdir first, then cwd root
358
+ loadEnvFile(path.join(cwd, 'e2e', 'credentials.env'));
359
+ loadEnvFile(path.join(cwd, 'credentials.env'));
360
+ }
361
+
133
362
  export async function loadConfig(cliArgs = {}, cwd = null) {
134
363
  cwd = cwd || process.cwd();
135
364
  loadDotEnv(cwd);
@@ -142,6 +371,24 @@ export async function loadConfig(cliArgs = {}, cwd = null) {
142
371
  ...envConfig,
143
372
  ...cliArgs,
144
373
  };
374
+
375
+ // Deep merge nested config objects
376
+ if (fileConfig.sync || envConfig.sync || cliArgs.sync) {
377
+ config.sync = deepMerge(
378
+ DEFAULTS.sync,
379
+ fileConfig.sync || {},
380
+ envConfig.sync || {},
381
+ cliArgs.sync || {}
382
+ );
383
+ }
384
+ if (fileConfig.appPool || envConfig.appPool || cliArgs.appPool) {
385
+ config.appPool = deepMerge(
386
+ DEFAULTS.appPool,
387
+ fileConfig.appPool || {},
388
+ envConfig.appPool || {},
389
+ cliArgs.appPool || {}
390
+ );
391
+ }
145
392
 
146
393
  // Apply environment profile overrides
147
394
  if (config.env && config.env !== 'default' && config.environments?.[config.env]) {
@@ -166,11 +413,25 @@ export async function loadConfig(cliArgs = {}, cwd = null) {
166
413
  fs.mkdirSync(config.screenshotsDir, { recursive: true });
167
414
  }
168
415
 
416
+ // Auto-infer authLoginEndpoint from baseUrl if credentials are available but no endpoint
417
+ if (config.authCredentials && !config.authLoginEndpoint && config.baseUrl) {
418
+ config.authLoginEndpoint = config.baseUrl.replace(/\/+$/, '') + '/api/auth/login';
419
+ }
420
+
169
421
  // Stash cwd for project identity (used by db.js)
170
422
  config._cwd = cwd;
171
423
  if (!config.projectName) {
172
424
  config.projectName = path.basename(cwd);
173
425
  }
174
426
 
427
+ // Normalize pool URLs: poolUrls array → _poolUrls, keep poolUrl as primary
428
+ if (config.poolUrls && Array.isArray(config.poolUrls) && config.poolUrls.length > 0) {
429
+ config._poolUrls = config.poolUrls;
430
+ config.poolUrl = config.poolUrls[0];
431
+ } else {
432
+ config._poolUrls = [config.poolUrl];
433
+ }
434
+ delete config.poolUrls;
435
+
175
436
  return config;
176
437
  }