@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.
- package/.claude-plugin/marketplace.json +52 -0
- package/.claude-plugin/plugin.json +17 -3
- package/.mcp.json +2 -2
- package/.opencode/commands/create-test.md +63 -0
- package/.opencode/commands/run.md +50 -0
- package/.opencode/commands/verify-issue.md +62 -0
- package/.opencode/skills/e2e-testing/SKILL.md +181 -0
- package/.opencode/skills/e2e-testing/references/action-types.md +143 -0
- package/.opencode/skills/e2e-testing/references/auth-strategies.md +91 -0
- package/.opencode/skills/e2e-testing/references/graphql.md +59 -0
- package/.opencode/skills/e2e-testing/references/issue-verification.md +59 -0
- package/.opencode/skills/e2e-testing/references/multi-pool.md +60 -0
- package/.opencode/skills/e2e-testing/references/network-debugging.md +62 -0
- package/.opencode/skills/e2e-testing/references/test-json-format.md +163 -0
- package/.opencode/skills/e2e-testing/references/troubleshooting.md +224 -0
- package/.opencode/skills/e2e-testing/references/variables.md +41 -0
- package/.opencode/skills/e2e-testing/references/visual-verification.md +89 -0
- package/LICENSE +190 -0
- package/OPENCODE.md +166 -0
- package/README.md +165 -104
- package/agents/test-creator.md +54 -1
- package/agents/test-improver.md +37 -0
- package/bin/cli.js +409 -16
- package/commands/capture.md +45 -0
- package/commands/create-test.md +16 -1
- package/opencode.json +11 -0
- package/package.json +7 -2
- package/scripts/setup-opencode.sh +113 -0
- package/skills/e2e-testing/SKILL.md +10 -3
- package/skills/e2e-testing/references/action-types.md +48 -5
- package/skills/e2e-testing/references/auth-strategies.md +91 -0
- package/skills/e2e-testing/references/graphql.md +59 -0
- package/skills/e2e-testing/references/issue-verification.md +59 -0
- package/skills/e2e-testing/references/multi-pool.md +60 -0
- package/skills/e2e-testing/references/network-debugging.md +62 -0
- package/skills/e2e-testing/references/test-json-format.md +4 -0
- package/skills/e2e-testing/references/troubleshooting.md +44 -2
- package/skills/e2e-testing/references/variables.md +41 -0
- package/skills/e2e-testing/references/visual-verification.md +89 -0
- package/src/actions.js +475 -2
- package/src/ai-generate.js +139 -8
- package/src/app-pool.js +339 -0
- package/src/config.js +266 -5
- package/src/dashboard.js +216 -17
- package/src/db.js +191 -7
- package/src/index.js +12 -9
- package/src/learner-sqlite.js +458 -0
- package/src/learner.js +78 -6
- package/src/mcp-tools.js +1348 -51
- package/src/module-resolver.js +37 -0
- package/src/narrate.js +65 -0
- package/src/pool-manager.js +229 -0
- package/src/pool.js +301 -31
- package/src/reporter.js +86 -2
- package/src/runner.js +480 -71
- package/src/sync/auth.js +354 -0
- package/src/sync/client.js +572 -0
- package/src/sync/hub-routes.js +816 -0
- package/src/sync/index.js +68 -0
- package/src/sync/middleware.js +347 -0
- package/src/sync/queue.js +209 -0
- package/src/sync/schema.js +540 -0
- package/src/verify.js +10 -7
- package/src/visual-diff.js +446 -0
- package/src/watch.js +384 -0
- package/templates/build-dashboard.js +47 -6
- package/templates/dashboard/js/api.js +62 -0
- package/templates/dashboard/js/init.js +13 -0
- package/templates/dashboard/js/keyboard.js +46 -0
- package/templates/dashboard/js/state.js +40 -0
- package/templates/dashboard/js/toast.js +41 -0
- package/templates/dashboard/js/utils.js +216 -0
- package/templates/dashboard/js/view-live.js +181 -0
- package/templates/dashboard/js/view-runs.js +676 -0
- package/templates/dashboard/js/view-tests.js +294 -0
- package/templates/dashboard/js/view-watch.js +242 -0
- package/templates/dashboard/js/websocket.js +116 -0
- package/templates/dashboard/styles/base.css +69 -0
- package/templates/dashboard/styles/components.css +117 -0
- package/templates/dashboard/styles/view-live.css +97 -0
- package/templates/dashboard/styles/view-runs.css +243 -0
- package/templates/dashboard/styles/view-tests.css +96 -0
- package/templates/dashboard/styles/view-watch.css +53 -0
- package/templates/dashboard/template.html +181 -100
- package/templates/dashboard.html +1614 -547
- package/templates/sample-test.json +0 -8
- package/templates/dashboard/app.js +0 -1152
- 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
|
|
111
|
-
function
|
|
112
|
-
|
|
113
|
-
|
|
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
|
}
|