@matware/e2e-runner 1.1.1 → 1.3.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/.claude-plugin/marketplace.json +21 -0
- package/.claude-plugin/plugin.json +9 -0
- package/.mcp.json +9 -0
- 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/OPENCODE.md +166 -0
- package/README.md +990 -296
- package/agents/test-analyzer.md +81 -0
- package/agents/test-creator.md +155 -0
- package/agents/test-improver.md +177 -0
- package/bin/cli.js +602 -22
- package/commands/create-test.md +65 -0
- package/commands/run.md +49 -0
- package/commands/verify-issue.md +63 -0
- package/opencode.json +11 -0
- package/package.json +15 -2
- package/scripts/setup-opencode.sh +113 -0
- package/skills/e2e-testing/SKILL.md +173 -0
- package/skills/e2e-testing/references/action-types.md +143 -0
- 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 +163 -0
- package/skills/e2e-testing/references/troubleshooting.md +224 -0
- package/skills/e2e-testing/references/variables.md +41 -0
- package/skills/e2e-testing/references/visual-verification.md +89 -0
- package/src/actions.js +597 -20
- package/src/ai-generate.js +142 -12
- package/src/config.js +171 -0
- package/src/dashboard.js +299 -17
- package/src/db.js +335 -13
- package/src/index.js +15 -8
- package/src/learner-markdown.js +177 -0
- package/src/learner-neo4j.js +255 -0
- package/src/learner-sqlite.js +658 -0
- package/src/learner.js +418 -0
- package/src/mcp-tools.js +1558 -50
- package/src/module-resolver.js +310 -0
- package/src/narrate.js +262 -0
- package/src/neo4j-pool.js +124 -0
- package/src/pool-manager.js +223 -0
- package/src/reporter.js +117 -3
- package/src/runner.js +274 -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 +14 -9
- package/src/watch.js +384 -0
- package/templates/build-dashboard.js +69 -0
- package/templates/dashboard/js/api.js +60 -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 +196 -0
- package/templates/dashboard/js/view-live.js +143 -0
- package/templates/dashboard/js/view-runs.js +572 -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 +110 -0
- package/templates/dashboard/styles/base.css +69 -0
- package/templates/dashboard/styles/components.css +110 -0
- package/templates/dashboard/styles/view-live.css +74 -0
- package/templates/dashboard/styles/view-runs.css +207 -0
- package/templates/dashboard/styles/view-tests.css +96 -0
- package/templates/dashboard/styles/view-watch.css +53 -0
- package/templates/dashboard/template.html +267 -0
- package/templates/dashboard.html +2171 -530
- package/templates/docker-compose-neo4j.yml +19 -0
- package/templates/e2e.config.js +3 -0
- package/templates/sample-test.json +0 -8
package/bin/cli.js
CHANGED
|
@@ -14,6 +14,8 @@
|
|
|
14
14
|
* e2e-runner pool status Show pool status
|
|
15
15
|
* e2e-runner pool restart Restart the pool
|
|
16
16
|
* e2e-runner dashboard Start the web dashboard
|
|
17
|
+
* e2e-runner watch --interval 15m Watch mode: scheduled test runs
|
|
18
|
+
* e2e-runner watch --git Watch mode: run on git changes
|
|
17
19
|
* e2e-runner capture <url> Capture a screenshot of any URL
|
|
18
20
|
* e2e-runner issue <url> Fetch issue and show details
|
|
19
21
|
* e2e-runner issue <url> --generate Generate test file via Claude API
|
|
@@ -29,15 +31,36 @@ import path from 'path';
|
|
|
29
31
|
import http from 'http';
|
|
30
32
|
import { fileURLToPath } from 'url';
|
|
31
33
|
import { loadConfig } from '../src/config.js';
|
|
32
|
-
import { startPool, stopPool, restartPool,
|
|
34
|
+
import { startPool, stopPool, restartPool, connectToPool } from '../src/pool.js';
|
|
35
|
+
import { getPoolUrls, getAggregatedPoolStatus, waitForAnyPool, selectPool } from '../src/pool-manager.js';
|
|
33
36
|
import { runTestsParallel, loadTestFile, loadTestSuite, loadAllSuites, listSuites } from '../src/runner.js';
|
|
34
|
-
import { generateReport, saveReport, printReport, persistRun } from '../src/reporter.js';
|
|
37
|
+
import { generateReport, saveReport, printReport, persistRun, printInsights } from '../src/reporter.js';
|
|
35
38
|
import { startDashboard } from '../src/dashboard.js';
|
|
39
|
+
import { startWatch } from '../src/watch.js';
|
|
36
40
|
import { fetchIssue } from '../src/issues.js';
|
|
37
41
|
import { buildPrompt, generateTests, hasApiKey } from '../src/ai-generate.js';
|
|
38
42
|
import { verifyIssue } from '../src/verify.js';
|
|
39
43
|
import { ensureProject, computeScreenshotHash, registerScreenshotHash } from '../src/db.js';
|
|
40
44
|
import { log, colors as C } from '../src/logger.js';
|
|
45
|
+
import { listModules } from '../src/module-resolver.js';
|
|
46
|
+
import { getLearningsSummary, getFlakySummary, getSelectorStability, getPageHealth, getApiHealth, getErrorPatterns, getTestTrends } from '../src/learner-sqlite.js';
|
|
47
|
+
import { startNeo4j, stopNeo4j, getNeo4jStatus } from '../src/neo4j-pool.js';
|
|
48
|
+
import {
|
|
49
|
+
generateApiKey,
|
|
50
|
+
generateTotpSecret,
|
|
51
|
+
generateTotpUri,
|
|
52
|
+
generateMasterKey,
|
|
53
|
+
hashApiKey,
|
|
54
|
+
migrateSyncSchema,
|
|
55
|
+
createInstance,
|
|
56
|
+
getInstance,
|
|
57
|
+
listInstances,
|
|
58
|
+
updateInstanceStatus,
|
|
59
|
+
getHubConnection,
|
|
60
|
+
getQueueStats,
|
|
61
|
+
getSyncClient,
|
|
62
|
+
pullRuns,
|
|
63
|
+
} from '../src/sync/index.js';
|
|
41
64
|
|
|
42
65
|
const __filename = fileURLToPath(import.meta.url);
|
|
43
66
|
const __dirname = path.dirname(__filename);
|
|
@@ -60,7 +83,9 @@ function parseCLIConfig() {
|
|
|
60
83
|
const cliArgs = {};
|
|
61
84
|
if (getFlag('--base-url')) cliArgs.baseUrl = getFlag('--base-url');
|
|
62
85
|
if (getFlag('--pool-url')) cliArgs.poolUrl = getFlag('--pool-url');
|
|
86
|
+
if (getFlag('--pool-urls')) cliArgs.poolUrls = getFlag('--pool-urls').split(',').map(u => u.trim()).filter(Boolean);
|
|
63
87
|
if (getFlag('--tests-dir')) cliArgs.testsDir = getFlag('--tests-dir');
|
|
88
|
+
if (getFlag('--modules-dir')) cliArgs.modulesDir = getFlag('--modules-dir');
|
|
64
89
|
if (getFlag('--screenshots-dir')) cliArgs.screenshotsDir = getFlag('--screenshots-dir');
|
|
65
90
|
if (getFlag('--concurrency')) cliArgs.concurrency = parseInt(getFlag('--concurrency'));
|
|
66
91
|
if (getFlag('--pool-port')) cliArgs.poolPort = parseInt(getFlag('--pool-port'));
|
|
@@ -75,8 +100,24 @@ function parseCLIConfig() {
|
|
|
75
100
|
if (getFlag('--dashboard-port')) cliArgs.dashboardPort = parseInt(getFlag('--dashboard-port'));
|
|
76
101
|
if (getFlag('--project-name')) cliArgs.projectName = getFlag('--project-name');
|
|
77
102
|
if (hasFlag('--fail-on-network-error')) cliArgs.failOnNetworkError = true;
|
|
103
|
+
if (getFlag('--action-retries')) cliArgs.actionRetries = parseInt(getFlag('--action-retries'));
|
|
104
|
+
if (getFlag('--action-retry-delay')) cliArgs.actionRetryDelay = parseInt(getFlag('--action-retry-delay'));
|
|
78
105
|
if (getFlag('--auth-token')) cliArgs.authToken = getFlag('--auth-token');
|
|
79
106
|
if (getFlag('--auth-storage-key')) cliArgs.authStorageKey = getFlag('--auth-storage-key');
|
|
107
|
+
if (getFlag('--test-type')) cliArgs.testType = getFlag('--test-type');
|
|
108
|
+
if (getFlag('--network-ignore-domains')) cliArgs.networkIgnoreDomains = getFlag('--network-ignore-domains').split(',').map(d => d.trim()).filter(Boolean);
|
|
109
|
+
if (getFlag('--auth-login-endpoint')) cliArgs.authLoginEndpoint = getFlag('--auth-login-endpoint');
|
|
110
|
+
if (getFlag('--auth-token-path')) cliArgs.authTokenPath = getFlag('--auth-token-path');
|
|
111
|
+
if (getFlag('--gql-endpoint')) cliArgs.gqlEndpoint = getFlag('--gql-endpoint');
|
|
112
|
+
if (getFlag('--gql-auth-header')) cliArgs.gqlAuthHeader = getFlag('--gql-auth-header');
|
|
113
|
+
if (getFlag('--gql-auth-key')) cliArgs.gqlAuthKey = getFlag('--gql-auth-key');
|
|
114
|
+
if (getFlag('--gql-auth-prefix')) cliArgs.gqlAuthPrefix = getFlag('--gql-auth-prefix');
|
|
115
|
+
if (getFlag('--verification-strictness')) {
|
|
116
|
+
const val = getFlag('--verification-strictness');
|
|
117
|
+
if (['strict', 'moderate', 'lenient'].includes(val)) {
|
|
118
|
+
cliArgs.verificationStrictness = val;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
80
121
|
return cliArgs;
|
|
81
122
|
}
|
|
82
123
|
|
|
@@ -96,6 +137,12 @@ ${C.bold}Usage:${C.reset}
|
|
|
96
137
|
e2e-runner dashboard Start the web dashboard
|
|
97
138
|
e2e-runner dashboard --port <port> Custom port (default: 8484)
|
|
98
139
|
|
|
140
|
+
e2e-runner watch --interval 15m Watch mode: run tests on schedule
|
|
141
|
+
e2e-runner watch --git Watch mode: run on git changes
|
|
142
|
+
e2e-runner watch --interval 15m --git Both triggers
|
|
143
|
+
e2e-runner watch --webhook <url> Notify on failure/recovery
|
|
144
|
+
e2e-runner watch --projects <file> Multi-project watch
|
|
145
|
+
|
|
99
146
|
e2e-runner capture <url> Capture a screenshot of any URL
|
|
100
147
|
e2e-runner capture <url> --full-page Capture full scrollable page
|
|
101
148
|
e2e-runner capture <url> --selector <sel> Wait for selector before capture
|
|
@@ -106,18 +153,36 @@ ${C.bold}Usage:${C.reset}
|
|
|
106
153
|
e2e-runner issue <url> --generate Generate test file via Claude API
|
|
107
154
|
e2e-runner issue <url> --verify Generate + run + report bug status
|
|
108
155
|
e2e-runner issue <url> --prompt Output the AI prompt (for piping)
|
|
156
|
+
e2e-runner issue <url> --test-type e2e|api Test category (default: e2e)
|
|
109
157
|
|
|
110
158
|
e2e-runner pool start Start the Chrome Pool
|
|
111
159
|
e2e-runner pool stop Stop the Chrome Pool
|
|
112
160
|
e2e-runner pool status Show pool status
|
|
113
161
|
e2e-runner pool restart Restart the Chrome Pool
|
|
114
162
|
|
|
163
|
+
e2e-runner learnings Show test learnings summary
|
|
164
|
+
e2e-runner learnings --query <q> Query: flaky, selectors, pages, apis, errors, trends
|
|
165
|
+
|
|
166
|
+
e2e-runner neo4j start Start the Neo4j knowledge graph
|
|
167
|
+
e2e-runner neo4j stop Stop the Neo4j container
|
|
168
|
+
e2e-runner neo4j status Show Neo4j status
|
|
169
|
+
|
|
170
|
+
e2e-runner sync status Show sync connection status
|
|
171
|
+
e2e-runner sync add-instance Register new agent (hub mode)
|
|
172
|
+
e2e-runner sync list-instances List registered agents (hub mode)
|
|
173
|
+
e2e-runner sync approve <id> Approve pending agent (hub mode)
|
|
174
|
+
e2e-runner sync revoke <id> Suspend an agent (hub mode)
|
|
175
|
+
e2e-runner sync push Process sync queue (agent mode)
|
|
176
|
+
e2e-runner sync pull Pull runs from hub (agent mode)
|
|
177
|
+
|
|
115
178
|
e2e-runner init Scaffold e2e/ in the current project
|
|
116
179
|
|
|
117
180
|
${C.bold}Options:${C.reset}
|
|
118
181
|
--base-url <url> App base URL (default: http://host.docker.internal:3000)
|
|
119
182
|
--pool-url <ws-url> Chrome Pool URL (default: ws://localhost:3333)
|
|
183
|
+
--pool-urls <urls> Multiple Chrome Pool URLs, comma-separated (distributes tests)
|
|
120
184
|
--tests-dir <dir> Tests directory (default: e2e/tests)
|
|
185
|
+
--modules-dir <dir> Reusable modules directory (default: e2e/modules)
|
|
121
186
|
--screenshots-dir <dir> Screenshots directory (default: e2e/screenshots)
|
|
122
187
|
--concurrency <n> Parallel test workers (default: 3)
|
|
123
188
|
--pool-port <port> Chrome Pool port (default: 3333)
|
|
@@ -130,6 +195,20 @@ ${C.bold}Options:${C.reset}
|
|
|
130
195
|
--env <name> Environment profile from config (default: default)
|
|
131
196
|
--project-name <name> Project display name for dashboard (default: directory name)
|
|
132
197
|
--fail-on-network-error Fail tests when network requests fail (e.g. ERR_CONNECTION_REFUSED)
|
|
198
|
+
--network-ignore-domains <d1,d2> Ignore network errors from these domains (comma-separated)
|
|
199
|
+
--auth-login-endpoint <url> Auto-login: POST credentials to this URL to get auth token
|
|
200
|
+
--auth-token-path <path> Dot-path to token in auth response (default: token)
|
|
201
|
+
--verification-strictness <level> Visual verification: strict, moderate (default), lenient
|
|
202
|
+
|
|
203
|
+
${C.bold}Watch Options:${C.reset}
|
|
204
|
+
--interval <time> Run interval: 15m, 1h, 30s (required for schedule mode)
|
|
205
|
+
--git Poll git for new commits
|
|
206
|
+
--git-branch <branch> Branch to watch (default: HEAD)
|
|
207
|
+
--git-interval <time> Git poll frequency (default: 30s)
|
|
208
|
+
--webhook <url> Webhook URL for notifications
|
|
209
|
+
--webhook-events <events> When to notify: failure (default), recovery, always
|
|
210
|
+
--projects <file.json> Multi-project config file
|
|
211
|
+
--no-run-on-start Skip initial run on startup
|
|
133
212
|
|
|
134
213
|
${C.bold}Config:${C.reset}
|
|
135
214
|
Looks for e2e.config.js or e2e.config.json in the current directory.
|
|
@@ -144,22 +223,24 @@ async function cmdRun() {
|
|
|
144
223
|
let tests = [];
|
|
145
224
|
let hooks = {};
|
|
146
225
|
|
|
226
|
+
const poolUrls = getPoolUrls(config);
|
|
147
227
|
console.log(`\n${C.bold}${C.cyan}@matware/e2e-runner${C.reset} v${pkg.version}`);
|
|
148
|
-
|
|
228
|
+
const poolDisplay = poolUrls.length > 1 ? `${poolUrls.length} pools` : config.poolUrl;
|
|
229
|
+
console.log(`${C.dim}Pool: ${poolDisplay} | Base: ${config.baseUrl} | Concurrency: ${config.concurrency}${C.reset}\n`);
|
|
149
230
|
|
|
150
231
|
if (hasFlag('--all')) {
|
|
151
|
-
const loaded = loadAllSuites(config.testsDir);
|
|
232
|
+
const loaded = loadAllSuites(config.testsDir, config.modulesDir, config.exclude);
|
|
152
233
|
tests = loaded.tests;
|
|
153
234
|
hooks = loaded.hooks;
|
|
154
235
|
} else if (getFlag('--suite')) {
|
|
155
236
|
const name = getFlag('--suite');
|
|
156
|
-
const loaded = loadTestSuite(name, config.testsDir);
|
|
237
|
+
const loaded = loadTestSuite(name, config.testsDir, config.modulesDir);
|
|
157
238
|
tests = loaded.tests;
|
|
158
239
|
hooks = loaded.hooks;
|
|
159
240
|
log('📋', `${C.cyan}${name}${C.reset} (${tests.length} tests)`);
|
|
160
241
|
} else if (getFlag('--tests')) {
|
|
161
242
|
const file = getFlag('--tests');
|
|
162
|
-
const loaded = loadTestFile(path.resolve(file));
|
|
243
|
+
const loaded = loadTestFile(path.resolve(file), config.modulesDir);
|
|
163
244
|
tests = loaded.tests;
|
|
164
245
|
hooks = loaded.hooks;
|
|
165
246
|
log('📋', `${C.cyan}${file}${C.reset} (${tests.length} tests)`);
|
|
@@ -182,8 +263,8 @@ async function cmdRun() {
|
|
|
182
263
|
}
|
|
183
264
|
|
|
184
265
|
// Verify pool connectivity
|
|
185
|
-
log('🔌',
|
|
186
|
-
const pressure = await
|
|
266
|
+
log('🔌', `Checking Chrome Pool${poolUrls.length > 1 ? 's' : ''}...`);
|
|
267
|
+
const pressure = await waitForAnyPool(poolUrls);
|
|
187
268
|
log('✅', `Pool ready (${pressure.running}/${pressure.maxConcurrent} sessions, queued: ${pressure.queued})`);
|
|
188
269
|
|
|
189
270
|
// Wire up live progress to dashboard if running
|
|
@@ -206,12 +287,17 @@ async function cmdRun() {
|
|
|
206
287
|
|
|
207
288
|
// Execute tests
|
|
208
289
|
console.log('');
|
|
209
|
-
|
|
290
|
+
// Derive suite name: --suite flag > --tests file basename > null (for --all/--inline)
|
|
291
|
+
let suiteName = getFlag('--suite') || null;
|
|
292
|
+
if (!suiteName && getFlag('--tests')) {
|
|
293
|
+
suiteName = path.basename(getFlag('--tests'), '.json');
|
|
294
|
+
}
|
|
210
295
|
const results = await runTestsParallel(tests, config, hooks);
|
|
211
296
|
const report = generateReport(results);
|
|
212
297
|
saveReport(report, config.screenshotsDir, config);
|
|
213
|
-
persistRun(report, config, suiteName);
|
|
298
|
+
await persistRun(report, config, suiteName);
|
|
214
299
|
printReport(report, config.screenshotsDir);
|
|
300
|
+
printInsights(report, config);
|
|
215
301
|
|
|
216
302
|
// Wait for the last dashboard broadcast (run:complete) to flush before exiting
|
|
217
303
|
if (_lastBroadcast) await _lastBroadcast;
|
|
@@ -230,6 +316,18 @@ async function cmdList() {
|
|
|
230
316
|
console.log(` ${C.dim}- ${test}${C.reset}`);
|
|
231
317
|
}
|
|
232
318
|
}
|
|
319
|
+
|
|
320
|
+
const modules = listModules(config.modulesDir);
|
|
321
|
+
if (modules.length > 0) {
|
|
322
|
+
console.log(`${C.bold}Available modules:${C.reset}\n`);
|
|
323
|
+
for (const mod of modules) {
|
|
324
|
+
const paramNames = mod.params.map(p => p.required ? p.name : `${C.dim}${p.name}?${C.reset}`).join(', ');
|
|
325
|
+
console.log(` ${C.cyan}${mod.name}${C.reset} (${paramNames})`);
|
|
326
|
+
if (mod.description) {
|
|
327
|
+
console.log(` ${C.dim}${mod.description}${C.reset}`);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}
|
|
233
331
|
console.log('');
|
|
234
332
|
}
|
|
235
333
|
|
|
@@ -252,15 +350,32 @@ async function cmdPool() {
|
|
|
252
350
|
break;
|
|
253
351
|
|
|
254
352
|
case 'status': {
|
|
255
|
-
const
|
|
353
|
+
const statusPoolUrls = getPoolUrls(config);
|
|
354
|
+
const aggregated = await getAggregatedPoolStatus(statusPoolUrls);
|
|
256
355
|
console.log(`\n${C.bold}Chrome Pool Status:${C.reset}\n`);
|
|
257
|
-
|
|
258
|
-
|
|
356
|
+
|
|
357
|
+
if (statusPoolUrls.length > 1) {
|
|
358
|
+
console.log(` Pools: ${aggregated.totalPools} (${aggregated.availableCount} available)`);
|
|
359
|
+
console.log(` Running: ${aggregated.totalRunning}/${aggregated.totalMaxConcurrent}`);
|
|
360
|
+
console.log(` Queued: ${aggregated.totalQueued}`);
|
|
361
|
+
console.log('');
|
|
362
|
+
for (const pool of aggregated.pools) {
|
|
363
|
+
const label = pool.available ? `${C.green}Available${C.reset}` : pool.error ? `${C.red}Offline${C.reset}` : `${C.red}Busy${C.reset}`;
|
|
364
|
+
console.log(` ${C.cyan}${pool.url}${C.reset}`);
|
|
365
|
+
console.log(` Status: ${label}${pool.error ? ` (${pool.error})` : ''}`);
|
|
366
|
+
console.log(` Running: ${pool.running}/${pool.maxConcurrent}`);
|
|
367
|
+
console.log(` Queued: ${pool.queued}`);
|
|
368
|
+
}
|
|
259
369
|
} else {
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
370
|
+
const pool = aggregated.pools[0];
|
|
371
|
+
if (pool.error) {
|
|
372
|
+
console.log(` ${C.red}Offline${C.reset}: ${pool.error}`);
|
|
373
|
+
} else {
|
|
374
|
+
console.log(` Status: ${pool.available ? `${C.green}Available${C.reset}` : `${C.red}Busy${C.reset}`}`);
|
|
375
|
+
console.log(` Running: ${pool.running}/${pool.maxConcurrent}`);
|
|
376
|
+
console.log(` Queued: ${pool.queued}`);
|
|
377
|
+
console.log(` Sessions: ${pool.sessions?.length ?? 0}`);
|
|
378
|
+
}
|
|
264
379
|
}
|
|
265
380
|
console.log('');
|
|
266
381
|
break;
|
|
@@ -279,6 +394,7 @@ function cmdInit() {
|
|
|
279
394
|
// Create directory structure
|
|
280
395
|
const dirs = [
|
|
281
396
|
path.join(cwd, 'e2e', 'tests'),
|
|
397
|
+
path.join(cwd, 'e2e', 'modules'),
|
|
282
398
|
path.join(cwd, 'e2e', 'screenshots'),
|
|
283
399
|
];
|
|
284
400
|
|
|
@@ -376,12 +492,14 @@ async function cmdCapture() {
|
|
|
376
492
|
|
|
377
493
|
console.log(`\n${C.bold}${C.cyan}@matware/e2e-runner${C.reset} v${pkg.version}`);
|
|
378
494
|
|
|
495
|
+
const capturePoolUrls = getPoolUrls(config);
|
|
379
496
|
log('🔌', 'Checking Chrome Pool...');
|
|
380
|
-
await
|
|
497
|
+
await waitForAnyPool(capturePoolUrls);
|
|
381
498
|
|
|
382
499
|
let browser;
|
|
383
500
|
try {
|
|
384
|
-
|
|
501
|
+
const capturePool = await selectPool(capturePoolUrls);
|
|
502
|
+
browser = await connectToPool(capturePool);
|
|
385
503
|
const page = await browser.newPage();
|
|
386
504
|
await page.setViewport(config.viewport);
|
|
387
505
|
|
|
@@ -436,11 +554,12 @@ async function cmdIssue() {
|
|
|
436
554
|
|
|
437
555
|
const cliArgs = parseCLIConfig();
|
|
438
556
|
const config = await loadConfig(cliArgs);
|
|
557
|
+
const testType = cliArgs.testType || 'e2e';
|
|
439
558
|
|
|
440
559
|
if (hasFlag('--prompt')) {
|
|
441
560
|
// Output AI prompt as JSON to stdout
|
|
442
561
|
const issue = fetchIssue(url);
|
|
443
|
-
const promptData = buildPrompt(issue, config);
|
|
562
|
+
const promptData = buildPrompt(issue, config, testType);
|
|
444
563
|
console.log(JSON.stringify(promptData, null, 2));
|
|
445
564
|
return;
|
|
446
565
|
}
|
|
@@ -455,6 +574,7 @@ async function cmdIssue() {
|
|
|
455
574
|
console.log(`\n${C.bold}${C.cyan}@matware/e2e-runner${C.reset} v${pkg.version}`);
|
|
456
575
|
log('🔍', 'Fetching issue...');
|
|
457
576
|
|
|
577
|
+
config.testType = testType;
|
|
458
578
|
const result = await verifyIssue(url, config);
|
|
459
579
|
const { issue, report, bugConfirmed } = result;
|
|
460
580
|
|
|
@@ -483,9 +603,9 @@ async function cmdIssue() {
|
|
|
483
603
|
|
|
484
604
|
const issue = fetchIssue(url);
|
|
485
605
|
log('📋', `${C.cyan}${issue.title}${C.reset}`);
|
|
486
|
-
log('🤖',
|
|
606
|
+
log('🤖', `Generating ${testType} tests via Claude API...`);
|
|
487
607
|
|
|
488
|
-
const { tests, suiteName } = await generateTests(issue, config);
|
|
608
|
+
const { tests, suiteName } = await generateTests(issue, config, testType);
|
|
489
609
|
|
|
490
610
|
if (!fs.existsSync(config.testsDir)) {
|
|
491
611
|
fs.mkdirSync(config.testsDir, { recursive: true });
|
|
@@ -516,6 +636,450 @@ async function cmdIssue() {
|
|
|
516
636
|
console.log('');
|
|
517
637
|
}
|
|
518
638
|
|
|
639
|
+
async function cmdWatch() {
|
|
640
|
+
const cliArgs = parseCLIConfig();
|
|
641
|
+
|
|
642
|
+
// Parse watch-specific flags
|
|
643
|
+
if (getFlag('--interval')) cliArgs.watchInterval = getFlag('--interval');
|
|
644
|
+
if (getFlag('--webhook')) cliArgs.watchWebhookUrl = getFlag('--webhook');
|
|
645
|
+
if (getFlag('--webhook-events')) cliArgs.watchWebhookEvents = getFlag('--webhook-events');
|
|
646
|
+
if (hasFlag('--git')) cliArgs.watchGitPoll = true;
|
|
647
|
+
if (getFlag('--git-branch')) cliArgs.watchGitBranch = getFlag('--git-branch');
|
|
648
|
+
if (getFlag('--git-interval')) cliArgs.watchGitInterval = getFlag('--git-interval');
|
|
649
|
+
if (hasFlag('--no-run-on-start')) cliArgs.watchRunOnStart = false;
|
|
650
|
+
|
|
651
|
+
// Multi-project file
|
|
652
|
+
const projectsFile = getFlag('--projects');
|
|
653
|
+
if (projectsFile) {
|
|
654
|
+
const resolved = path.resolve(projectsFile);
|
|
655
|
+
if (!fs.existsSync(resolved)) {
|
|
656
|
+
console.error(`${C.red}Projects file not found: ${resolved}${C.reset}`);
|
|
657
|
+
process.exit(1);
|
|
658
|
+
}
|
|
659
|
+
cliArgs.watchProjects = JSON.parse(fs.readFileSync(resolved, 'utf-8'));
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
const config = await loadConfig(cliArgs);
|
|
663
|
+
|
|
664
|
+
console.log(`\n${C.bold}${C.cyan}@matware/e2e-runner${C.reset} v${pkg.version}`);
|
|
665
|
+
console.log(`${C.dim}Watch mode — dashboard on port ${config.dashboardPort}${C.reset}\n`);
|
|
666
|
+
|
|
667
|
+
const handle = await startWatch(config);
|
|
668
|
+
|
|
669
|
+
// Graceful shutdown
|
|
670
|
+
const shutdown = () => {
|
|
671
|
+
console.log(`\n${C.dim}Stopping watch...${C.reset}`);
|
|
672
|
+
handle.stop();
|
|
673
|
+
process.exit(0);
|
|
674
|
+
};
|
|
675
|
+
process.on('SIGINT', shutdown);
|
|
676
|
+
process.on('SIGTERM', shutdown);
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
async function cmdLearnings() {
|
|
680
|
+
const cliArgs = parseCLIConfig();
|
|
681
|
+
const config = await loadConfig(cliArgs);
|
|
682
|
+
const projectId = ensureProject(config._cwd, config.projectName, config.screenshotsDir, config.testsDir);
|
|
683
|
+
const days = config.learningsDays || 30;
|
|
684
|
+
const query = getFlag('--query') || 'summary';
|
|
685
|
+
|
|
686
|
+
console.log(`\n${C.bold}${C.cyan}@matware/e2e-runner${C.reset} v${pkg.version}`);
|
|
687
|
+
console.log(`${C.dim}Project: ${config.projectName} | Analysis window: ${days} days${C.reset}\n`);
|
|
688
|
+
|
|
689
|
+
switch (query) {
|
|
690
|
+
case 'summary': {
|
|
691
|
+
const summary = getLearningsSummary(projectId);
|
|
692
|
+
if (summary.totalRuns === 0) {
|
|
693
|
+
console.log(`${C.dim}No learnings data yet. Run some tests to start building knowledge.${C.reset}\n`);
|
|
694
|
+
return;
|
|
695
|
+
}
|
|
696
|
+
console.log(`${C.bold}Health Overview${C.reset}`);
|
|
697
|
+
console.log(`${'─'.repeat(50)}`);
|
|
698
|
+
console.log(` Total Runs: ${C.bold}${summary.totalRuns}${C.reset}`);
|
|
699
|
+
console.log(` Total Tests: ${C.bold}${summary.totalTests}${C.reset}`);
|
|
700
|
+
console.log(` Pass Rate: ${summary.overallPassRate >= 90 ? C.green : summary.overallPassRate >= 70 ? '' : C.red}${summary.overallPassRate}%${C.reset}`);
|
|
701
|
+
console.log(` Avg Duration: ${summary.avgDurationMs < 1000 ? summary.avgDurationMs + 'ms' : (summary.avgDurationMs / 1000).toFixed(1) + 's'}`);
|
|
702
|
+
console.log(` Flaky Tests: ${summary.flakyTests.length > 0 ? C.red : C.green}${summary.flakyTests.length}${C.reset}`);
|
|
703
|
+
console.log(` Unstable Selectors: ${summary.unstableSelectors.length > 0 ? C.red : C.green}${summary.unstableSelectors.length}${C.reset}`);
|
|
704
|
+
|
|
705
|
+
if (summary.flakyTests.length > 0) {
|
|
706
|
+
console.log(`\n${C.bold}Top Flaky Tests${C.reset}`);
|
|
707
|
+
summary.flakyTests.slice(0, 5).forEach(f => {
|
|
708
|
+
console.log(` ${C.yellow}⚠${C.reset} ${f.test_name} — ${f.flaky_rate}% flaky`);
|
|
709
|
+
});
|
|
710
|
+
}
|
|
711
|
+
if (summary.topErrors.length > 0) {
|
|
712
|
+
console.log(`\n${C.bold}Top Errors${C.reset}`);
|
|
713
|
+
summary.topErrors.slice(0, 5).forEach(e => {
|
|
714
|
+
console.log(` ${C.red}✗${C.reset} [${e.category}] ${e.pattern.slice(0, 60)}${e.pattern.length > 60 ? '...' : ''} (${e.occurrence_count}x)`);
|
|
715
|
+
});
|
|
716
|
+
}
|
|
717
|
+
console.log('');
|
|
718
|
+
break;
|
|
719
|
+
}
|
|
720
|
+
case 'flaky': {
|
|
721
|
+
const flaky = getFlakySummary(projectId, days);
|
|
722
|
+
if (flaky.length === 0) { console.log(`${C.green}No flaky tests found.${C.reset}\n`); return; }
|
|
723
|
+
console.log(`${C.bold}Flaky Tests${C.reset}\n`);
|
|
724
|
+
flaky.forEach(f => {
|
|
725
|
+
console.log(` ${C.yellow}⚠${C.reset} ${C.bold}${f.test_name}${C.reset}`);
|
|
726
|
+
console.log(` Rate: ${f.flaky_rate}% | Occurrences: ${f.flaky_count}/${f.total_runs} | Avg attempts: ${f.avg_attempts}`);
|
|
727
|
+
});
|
|
728
|
+
console.log('');
|
|
729
|
+
break;
|
|
730
|
+
}
|
|
731
|
+
case 'selectors': {
|
|
732
|
+
const sels = getSelectorStability(projectId, days);
|
|
733
|
+
if (sels.length === 0) { console.log(`${C.green}All selectors are stable.${C.reset}\n`); return; }
|
|
734
|
+
console.log(`${C.bold}Unstable Selectors${C.reset}\n`);
|
|
735
|
+
sels.forEach(s => {
|
|
736
|
+
console.log(` ${C.red}✗${C.reset} ${C.dim}${s.selector}${C.reset}`);
|
|
737
|
+
console.log(` Action: ${s.action_type} | Fail: ${s.fail_rate}% | Uses: ${s.total_uses} | Tests: ${s.used_by_tests}`);
|
|
738
|
+
});
|
|
739
|
+
console.log('');
|
|
740
|
+
break;
|
|
741
|
+
}
|
|
742
|
+
case 'pages': {
|
|
743
|
+
const pages = getPageHealth(projectId, days);
|
|
744
|
+
const failing = pages.filter(p => p.fail_rate > 0);
|
|
745
|
+
if (failing.length === 0) { console.log(`${C.green}All pages are healthy.${C.reset}\n`); return; }
|
|
746
|
+
console.log(`${C.bold}Failing Pages${C.reset}\n`);
|
|
747
|
+
failing.forEach(p => {
|
|
748
|
+
console.log(` ${C.red}✗${C.reset} ${C.bold}${p.url_path}${C.reset}`);
|
|
749
|
+
console.log(` Fail: ${p.fail_rate}% | Visits: ${p.total_visits} | Console errors: ${p.console_errors} | Network errors: ${p.network_errors}`);
|
|
750
|
+
});
|
|
751
|
+
console.log('');
|
|
752
|
+
break;
|
|
753
|
+
}
|
|
754
|
+
case 'apis': {
|
|
755
|
+
const apis = getApiHealth(projectId, days);
|
|
756
|
+
const issues = apis.filter(a => a.error_rate > 0);
|
|
757
|
+
if (issues.length === 0) { console.log(`${C.green}All API endpoints are healthy.${C.reset}\n`); return; }
|
|
758
|
+
console.log(`${C.bold}API Issues${C.reset}\n`);
|
|
759
|
+
issues.forEach(a => {
|
|
760
|
+
console.log(` ${C.red}✗${C.reset} ${C.bold}${a.endpoint}${C.reset}`);
|
|
761
|
+
console.log(` Error: ${a.error_rate}% | Calls: ${a.total_calls} | Avg: ${Math.round(a.avg_duration_ms)}ms | Status: ${a.status_codes}`);
|
|
762
|
+
});
|
|
763
|
+
console.log('');
|
|
764
|
+
break;
|
|
765
|
+
}
|
|
766
|
+
case 'errors': {
|
|
767
|
+
const errors = getErrorPatterns(projectId);
|
|
768
|
+
if (errors.length === 0) { console.log(`${C.green}No error patterns recorded.${C.reset}\n`); return; }
|
|
769
|
+
console.log(`${C.bold}Error Patterns${C.reset}\n`);
|
|
770
|
+
errors.forEach(e => {
|
|
771
|
+
console.log(` ${C.red}✗${C.reset} [${e.category}] ${e.pattern.slice(0, 70)}${e.pattern.length > 70 ? '...' : ''}`);
|
|
772
|
+
console.log(` Count: ${e.occurrence_count} | Last: ${(e.last_seen || '').split('T')[0]} | Test: ${e.example_test || '-'}`);
|
|
773
|
+
});
|
|
774
|
+
console.log('');
|
|
775
|
+
break;
|
|
776
|
+
}
|
|
777
|
+
case 'trends': {
|
|
778
|
+
const trends = getTestTrends(projectId, days);
|
|
779
|
+
if (trends.length === 0) { console.log(`${C.dim}No trend data available.${C.reset}\n`); return; }
|
|
780
|
+
console.log(`${C.bold}Test Trends (${days} days)${C.reset}\n`);
|
|
781
|
+
console.log(` ${'Date'.padEnd(12)} ${'Pass Rate'.padEnd(11)} ${'Tests'.padEnd(7)} ${'Pass'.padEnd(6)} ${'Fail'.padEnd(6)} Flaky`);
|
|
782
|
+
console.log(` ${'─'.repeat(55)}`);
|
|
783
|
+
trends.forEach(t => {
|
|
784
|
+
const rateColor = t.pass_rate >= 90 ? C.green : t.pass_rate >= 70 ? '' : C.red;
|
|
785
|
+
console.log(` ${t.date.padEnd(12)} ${rateColor}${(t.pass_rate + '%').padEnd(11)}${C.reset} ${String(t.total_tests).padEnd(7)} ${C.green}${String(t.passed).padEnd(6)}${C.reset} ${t.failed > 0 ? C.red : ''}${String(t.failed).padEnd(6)}${C.reset} ${t.flaky_count}`);
|
|
786
|
+
});
|
|
787
|
+
console.log('');
|
|
788
|
+
break;
|
|
789
|
+
}
|
|
790
|
+
default:
|
|
791
|
+
console.error(`${C.red}Unknown query: ${query}. Available: summary, flaky, selectors, pages, apis, errors, trends${C.reset}`);
|
|
792
|
+
process.exit(1);
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
async function cmdNeo4j() {
|
|
797
|
+
const subCmd = args[1];
|
|
798
|
+
const cliArgs = parseCLIConfig();
|
|
799
|
+
const config = await loadConfig(cliArgs);
|
|
800
|
+
|
|
801
|
+
switch (subCmd) {
|
|
802
|
+
case 'start':
|
|
803
|
+
startNeo4j(config);
|
|
804
|
+
break;
|
|
805
|
+
case 'stop':
|
|
806
|
+
stopNeo4j(config);
|
|
807
|
+
break;
|
|
808
|
+
case 'status': {
|
|
809
|
+
const status = getNeo4jStatus(config);
|
|
810
|
+
console.log(`\n${C.bold}Neo4j Status:${C.reset}\n`);
|
|
811
|
+
if (status.running) {
|
|
812
|
+
console.log(` Status: ${C.green}Running${C.reset}`);
|
|
813
|
+
console.log(` Bolt: ${C.cyan}bolt://localhost:${status.boltPort}${C.reset}`);
|
|
814
|
+
console.log(` Browser: ${C.cyan}http://localhost:${status.httpPort}${C.reset}`);
|
|
815
|
+
} else {
|
|
816
|
+
console.log(` Status: ${C.red}Stopped${C.reset}`);
|
|
817
|
+
if (status.error) console.log(` ${C.dim}${status.error}${C.reset}`);
|
|
818
|
+
}
|
|
819
|
+
console.log('');
|
|
820
|
+
break;
|
|
821
|
+
}
|
|
822
|
+
default:
|
|
823
|
+
console.error(`${C.red}Unknown subcommand: ${subCmd}. Available: start, stop, status${C.reset}`);
|
|
824
|
+
process.exit(1);
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
// ==================== Sync ====================
|
|
829
|
+
|
|
830
|
+
async function cmdSync() {
|
|
831
|
+
const subCmd = args[1];
|
|
832
|
+
const cliArgs = parseCLIConfig();
|
|
833
|
+
const config = await loadConfig(cliArgs);
|
|
834
|
+
|
|
835
|
+
// Ensure schema is migrated
|
|
836
|
+
migrateSyncSchema();
|
|
837
|
+
|
|
838
|
+
switch (subCmd) {
|
|
839
|
+
case 'status': {
|
|
840
|
+
const mode = config.sync?.mode || 'standalone';
|
|
841
|
+
console.log(`\n${C.bold}Sync Status:${C.reset}\n`);
|
|
842
|
+
console.log(` Mode: ${C.cyan}${mode}${C.reset}`);
|
|
843
|
+
|
|
844
|
+
if (mode === 'hub') {
|
|
845
|
+
const instances = listInstances();
|
|
846
|
+
const active = instances.filter(i => i.status === 'active').length;
|
|
847
|
+
const online = instances.filter(i => {
|
|
848
|
+
if (!i.last_seen) return false;
|
|
849
|
+
const lastSeen = new Date(i.last_seen + 'Z').getTime();
|
|
850
|
+
return Date.now() - lastSeen < 5 * 60 * 1000;
|
|
851
|
+
}).length;
|
|
852
|
+
console.log(` Instances: ${instances.length} total, ${active} active, ${online} online`);
|
|
853
|
+
} else if (mode === 'agent') {
|
|
854
|
+
const conn = getHubConnection();
|
|
855
|
+
if (conn) {
|
|
856
|
+
console.log(` Hub URL: ${C.cyan}${conn.hub_url}${C.reset}`);
|
|
857
|
+
console.log(` Instance: ${conn.instance_id}`);
|
|
858
|
+
console.log(` Status: ${conn.status === 'connected' ? C.green : C.red}${conn.status}${C.reset}`);
|
|
859
|
+
console.log(` Last push: ${conn.last_push || 'never'}`);
|
|
860
|
+
console.log(` Last pull: ${conn.last_pull || 'never'}`);
|
|
861
|
+
} else {
|
|
862
|
+
console.log(` ${C.dim}Not connected to any hub${C.reset}`);
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
const queueStats = getQueueStats();
|
|
866
|
+
if (queueStats.length > 0) {
|
|
867
|
+
const pending = queueStats.find(s => s.status === 'pending')?.count || 0;
|
|
868
|
+
if (pending > 0) {
|
|
869
|
+
console.log(` Queue: ${C.yellow}${pending} pending${C.reset}`);
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
console.log('');
|
|
874
|
+
break;
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
case 'add-instance': {
|
|
878
|
+
if (config.sync?.mode !== 'hub') {
|
|
879
|
+
console.error(`${C.red}Error: This command only works in hub mode${C.reset}`);
|
|
880
|
+
process.exit(1);
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
const instanceId = getFlag('--id') || `instance-${Date.now().toString(36)}`;
|
|
884
|
+
const displayName = getFlag('--name') || instanceId;
|
|
885
|
+
const role = getFlag('--role') || 'member';
|
|
886
|
+
const environment = getFlag('--env') || 'development';
|
|
887
|
+
|
|
888
|
+
// Check if already exists
|
|
889
|
+
if (getInstance(instanceId)) {
|
|
890
|
+
console.error(`${C.red}Error: Instance '${instanceId}' already exists${C.reset}`);
|
|
891
|
+
process.exit(1);
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
// Generate credentials
|
|
895
|
+
const apiKey = generateApiKey();
|
|
896
|
+
const totpSecret = generateTotpSecret();
|
|
897
|
+
|
|
898
|
+
// Create instance
|
|
899
|
+
createInstance({
|
|
900
|
+
instanceId,
|
|
901
|
+
displayName,
|
|
902
|
+
hostname: null,
|
|
903
|
+
environment,
|
|
904
|
+
apiKeyHash: hashApiKey(apiKey),
|
|
905
|
+
totpSecret,
|
|
906
|
+
role,
|
|
907
|
+
status: config.sync?.hub?.requireApproval ? 'pending' : 'active',
|
|
908
|
+
});
|
|
909
|
+
|
|
910
|
+
console.log(`\n${C.green}${C.bold}Instance created successfully!${C.reset}\n`);
|
|
911
|
+
console.log(`${C.bold}Instance ID:${C.reset} ${instanceId}`);
|
|
912
|
+
console.log(`${C.bold}Display Name:${C.reset} ${displayName}`);
|
|
913
|
+
console.log(`${C.bold}Role:${C.reset} ${role}`);
|
|
914
|
+
console.log(`${C.bold}Status:${C.reset} ${config.sync?.hub?.requireApproval ? 'pending' : 'active'}`);
|
|
915
|
+
console.log('');
|
|
916
|
+
console.log(`${C.bold}${C.yellow}SAVE THESE CREDENTIALS (shown only once):${C.reset}`);
|
|
917
|
+
console.log(`${C.bold}API Key:${C.reset} ${apiKey}`);
|
|
918
|
+
console.log(`${C.bold}TOTP Secret:${C.reset} ${totpSecret}`);
|
|
919
|
+
console.log(`${C.bold}TOTP URI:${C.reset} ${generateTotpUri(totpSecret, instanceId)}`);
|
|
920
|
+
console.log('');
|
|
921
|
+
console.log(`${C.dim}Configure the agent with:${C.reset}`);
|
|
922
|
+
console.log(` export E2E_SYNC_API_KEY="${apiKey}"`);
|
|
923
|
+
console.log(` export E2E_SYNC_TOTP="${totpSecret}"`);
|
|
924
|
+
console.log('');
|
|
925
|
+
break;
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
case 'list-instances': {
|
|
929
|
+
if (config.sync?.mode !== 'hub') {
|
|
930
|
+
console.error(`${C.red}Error: This command only works in hub mode${C.reset}`);
|
|
931
|
+
process.exit(1);
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
const status = getFlag('--status');
|
|
935
|
+
const instances = listInstances(status);
|
|
936
|
+
|
|
937
|
+
console.log(`\n${C.bold}Registered Instances:${C.reset}\n`);
|
|
938
|
+
|
|
939
|
+
if (instances.length === 0) {
|
|
940
|
+
console.log(` ${C.dim}No instances registered${C.reset}`);
|
|
941
|
+
} else {
|
|
942
|
+
for (const inst of instances) {
|
|
943
|
+
const isOnline = inst.last_seen && (Date.now() - new Date(inst.last_seen + 'Z').getTime() < 5 * 60 * 1000);
|
|
944
|
+
const statusColor = inst.status === 'active' ? C.green : inst.status === 'pending' ? C.yellow : C.red;
|
|
945
|
+
const onlineIndicator = isOnline ? `${C.green}*${C.reset}` : ' ';
|
|
946
|
+
|
|
947
|
+
console.log(` ${onlineIndicator} ${C.bold}${inst.instance_id}${C.reset}`);
|
|
948
|
+
console.log(` Name: ${inst.display_name}`);
|
|
949
|
+
console.log(` Status: ${statusColor}${inst.status}${C.reset}`);
|
|
950
|
+
console.log(` Role: ${inst.role}`);
|
|
951
|
+
console.log(` Seen: ${inst.last_seen || 'never'}`);
|
|
952
|
+
console.log('');
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
break;
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
case 'approve': {
|
|
959
|
+
if (config.sync?.mode !== 'hub') {
|
|
960
|
+
console.error(`${C.red}Error: This command only works in hub mode${C.reset}`);
|
|
961
|
+
process.exit(1);
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
const instanceId = args[2];
|
|
965
|
+
if (!instanceId) {
|
|
966
|
+
console.error(`${C.red}Error: Instance ID required${C.reset}`);
|
|
967
|
+
process.exit(1);
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
const instance = getInstance(instanceId);
|
|
971
|
+
if (!instance) {
|
|
972
|
+
console.error(`${C.red}Error: Instance '${instanceId}' not found${C.reset}`);
|
|
973
|
+
process.exit(1);
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
updateInstanceStatus(instanceId, 'active');
|
|
977
|
+
console.log(`${C.green}Instance '${instanceId}' approved and activated${C.reset}`);
|
|
978
|
+
break;
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
case 'revoke': {
|
|
982
|
+
if (config.sync?.mode !== 'hub') {
|
|
983
|
+
console.error(`${C.red}Error: This command only works in hub mode${C.reset}`);
|
|
984
|
+
process.exit(1);
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
const instanceId = args[2];
|
|
988
|
+
if (!instanceId) {
|
|
989
|
+
console.error(`${C.red}Error: Instance ID required${C.reset}`);
|
|
990
|
+
process.exit(1);
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
updateInstanceStatus(instanceId, 'suspended');
|
|
994
|
+
console.log(`${C.yellow}Instance '${instanceId}' suspended${C.reset}`);
|
|
995
|
+
break;
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
case 'push': {
|
|
999
|
+
if (config.sync?.mode !== 'agent') {
|
|
1000
|
+
console.error(`${C.red}Error: This command only works in agent mode${C.reset}`);
|
|
1001
|
+
process.exit(1);
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
const client = await getSyncClient(config);
|
|
1005
|
+
if (!client.isConfigured()) {
|
|
1006
|
+
console.error(`${C.red}Error: Sync credentials not configured${C.reset}`);
|
|
1007
|
+
console.log(`${C.dim}Set E2E_SYNC_API_KEY and E2E_SYNC_TOTP environment variables${C.reset}`);
|
|
1008
|
+
process.exit(1);
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
console.log('Processing sync queue...');
|
|
1012
|
+
await client.processQueue();
|
|
1013
|
+
console.log(`${C.green}Queue processed${C.reset}`);
|
|
1014
|
+
break;
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
case 'pull': {
|
|
1018
|
+
if (config.sync?.mode !== 'agent') {
|
|
1019
|
+
console.error(`${C.red}Error: This command only works in agent mode${C.reset}`);
|
|
1020
|
+
process.exit(1);
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
const since = getFlag('--since');
|
|
1024
|
+
const project = getFlag('--project');
|
|
1025
|
+
const limit = getFlag('--limit') ? parseInt(getFlag('--limit')) : 50;
|
|
1026
|
+
|
|
1027
|
+
console.log('Pulling runs from hub...');
|
|
1028
|
+
const result = await pullRuns(config, { since, project, limit });
|
|
1029
|
+
|
|
1030
|
+
if (result) {
|
|
1031
|
+
console.log(`${C.green}Pulled ${result.runs?.length || 0} runs${C.reset}`);
|
|
1032
|
+
|
|
1033
|
+
if (result.runs?.length > 0) {
|
|
1034
|
+
console.log('');
|
|
1035
|
+
for (const run of result.runs.slice(0, 10)) {
|
|
1036
|
+
const status = run.failed > 0 ? C.red + 'FAIL' : C.green + 'PASS';
|
|
1037
|
+
console.log(` ${status}${C.reset} ${run.project_name} - ${run.suite_name || 'default'} (${run.passed}/${run.total})`);
|
|
1038
|
+
}
|
|
1039
|
+
if (result.runs.length > 10) {
|
|
1040
|
+
console.log(` ${C.dim}... and ${result.runs.length - 10} more${C.reset}`);
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
} else {
|
|
1044
|
+
console.log(`${C.yellow}No runs pulled (check configuration)${C.reset}`);
|
|
1045
|
+
}
|
|
1046
|
+
break;
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
case 'generate-master-key': {
|
|
1050
|
+
const key = generateMasterKey();
|
|
1051
|
+
console.log(`\n${C.bold}Generated Master Key:${C.reset}\n`);
|
|
1052
|
+
console.log(` ${key}`);
|
|
1053
|
+
console.log('');
|
|
1054
|
+
console.log(`${C.dim}Set this in your hub environment:${C.reset}`);
|
|
1055
|
+
console.log(` export E2E_SYNC_MASTER_KEY="${key}"`);
|
|
1056
|
+
console.log('');
|
|
1057
|
+
break;
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
default:
|
|
1061
|
+
console.log(`\n${C.bold}Sync Commands:${C.reset}\n`);
|
|
1062
|
+
console.log(' status Show sync status');
|
|
1063
|
+
console.log(' add-instance Register a new agent (hub mode)');
|
|
1064
|
+
console.log(' list-instances List registered agents (hub mode)');
|
|
1065
|
+
console.log(' approve <id> Approve pending agent (hub mode)');
|
|
1066
|
+
console.log(' revoke <id> Suspend an agent (hub mode)');
|
|
1067
|
+
console.log(' push Process sync queue (agent mode)');
|
|
1068
|
+
console.log(' pull Pull runs from hub (agent mode)');
|
|
1069
|
+
console.log(' generate-master-key Generate encryption master key');
|
|
1070
|
+
console.log('');
|
|
1071
|
+
console.log(`${C.bold}Options:${C.reset}`);
|
|
1072
|
+
console.log(' --id <id> Instance ID for add-instance');
|
|
1073
|
+
console.log(' --name <name> Display name for add-instance');
|
|
1074
|
+
console.log(' --role <role> Role: admin, member, readonly');
|
|
1075
|
+
console.log(' --status <status> Filter by status: pending, active, suspended');
|
|
1076
|
+
console.log(' --since <datetime> Pull runs since timestamp');
|
|
1077
|
+
console.log(' --project <slug> Filter by project');
|
|
1078
|
+
console.log(' --limit <n> Limit number of runs to pull');
|
|
1079
|
+
console.log('');
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
|
|
519
1083
|
// ==================== Main ====================
|
|
520
1084
|
|
|
521
1085
|
async function main() {
|
|
@@ -548,6 +1112,10 @@ async function main() {
|
|
|
548
1112
|
await cmdDashboard();
|
|
549
1113
|
break;
|
|
550
1114
|
|
|
1115
|
+
case 'watch':
|
|
1116
|
+
await cmdWatch();
|
|
1117
|
+
break;
|
|
1118
|
+
|
|
551
1119
|
case 'capture':
|
|
552
1120
|
await cmdCapture();
|
|
553
1121
|
break;
|
|
@@ -556,6 +1124,18 @@ async function main() {
|
|
|
556
1124
|
await cmdIssue();
|
|
557
1125
|
break;
|
|
558
1126
|
|
|
1127
|
+
case 'learnings':
|
|
1128
|
+
await cmdLearnings();
|
|
1129
|
+
break;
|
|
1130
|
+
|
|
1131
|
+
case 'neo4j':
|
|
1132
|
+
await cmdNeo4j();
|
|
1133
|
+
break;
|
|
1134
|
+
|
|
1135
|
+
case 'sync':
|
|
1136
|
+
await cmdSync();
|
|
1137
|
+
break;
|
|
1138
|
+
|
|
559
1139
|
case 'init':
|
|
560
1140
|
cmdInit();
|
|
561
1141
|
break;
|