@hasna/testers 0.0.8 → 0.0.10
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/dashboard/dist/assets/index-DvYdwJK-.css +1 -0
- package/dashboard/dist/assets/index-RV9LMdfY.js +49 -0
- package/dashboard/dist/index.html +2 -2
- package/dist/cli/index.js +1191 -354
- package/dist/db/results.d.ts +1 -0
- package/dist/db/results.d.ts.map +1 -1
- package/dist/db/runs.d.ts +1 -0
- package/dist/db/runs.d.ts.map +1 -1
- package/dist/db/scenarios.d.ts +1 -0
- package/dist/db/scenarios.d.ts.map +1 -1
- package/dist/db/screenshots.d.ts +1 -0
- package/dist/db/screenshots.d.ts.map +1 -1
- package/dist/index.js +256 -21
- package/dist/lib/costs.d.ts +1 -0
- package/dist/lib/costs.d.ts.map +1 -1
- package/dist/lib/init.d.ts.map +1 -1
- package/dist/lib/logs-integration.d.ts +7 -0
- package/dist/lib/logs-integration.d.ts.map +1 -0
- package/dist/lib/reporter.d.ts +7 -0
- package/dist/lib/reporter.d.ts.map +1 -1
- package/dist/lib/runner.d.ts +3 -0
- package/dist/lib/runner.d.ts.map +1 -1
- package/dist/mcp/index.js +45 -2
- package/dist/server/index.js +4172 -16
- package/package.json +1 -1
- package/dashboard/dist/assets/index-CDcHt94n.css +0 -1
- package/dashboard/dist/assets/index-DCNDCh61.js +0 -49
package/dist/cli/index.js
CHANGED
|
@@ -2666,7 +2666,8 @@ __export(exports_runs, {
|
|
|
2666
2666
|
listRuns: () => listRuns,
|
|
2667
2667
|
getRun: () => getRun,
|
|
2668
2668
|
deleteRun: () => deleteRun,
|
|
2669
|
-
createRun: () => createRun
|
|
2669
|
+
createRun: () => createRun,
|
|
2670
|
+
countRuns: () => countRuns
|
|
2670
2671
|
});
|
|
2671
2672
|
function createRun(input) {
|
|
2672
2673
|
const db2 = getDatabase();
|
|
@@ -2719,6 +2720,24 @@ function listRuns(filter) {
|
|
|
2719
2720
|
const rows = db2.query(sql).all(...params);
|
|
2720
2721
|
return rows.map(runFromRow);
|
|
2721
2722
|
}
|
|
2723
|
+
function countRuns(filter) {
|
|
2724
|
+
const db2 = getDatabase();
|
|
2725
|
+
const conditions = [];
|
|
2726
|
+
const params = [];
|
|
2727
|
+
if (filter?.projectId) {
|
|
2728
|
+
conditions.push("project_id = ?");
|
|
2729
|
+
params.push(filter.projectId);
|
|
2730
|
+
}
|
|
2731
|
+
if (filter?.status) {
|
|
2732
|
+
conditions.push("status = ?");
|
|
2733
|
+
params.push(filter.status);
|
|
2734
|
+
}
|
|
2735
|
+
let sql = "SELECT COUNT(*) as count FROM runs";
|
|
2736
|
+
if (conditions.length > 0)
|
|
2737
|
+
sql += " WHERE " + conditions.join(" AND ");
|
|
2738
|
+
const row = db2.query(sql).get(...params);
|
|
2739
|
+
return row.count;
|
|
2740
|
+
}
|
|
2722
2741
|
function updateRun(id, updates) {
|
|
2723
2742
|
const db2 = getDatabase();
|
|
2724
2743
|
const existing = getRun(id);
|
|
@@ -3383,11 +3402,95 @@ var {
|
|
|
3383
3402
|
Help
|
|
3384
3403
|
} = import__.default;
|
|
3385
3404
|
|
|
3405
|
+
// src/cli/index.tsx
|
|
3406
|
+
import chalk5 from "chalk";
|
|
3407
|
+
// package.json
|
|
3408
|
+
var package_default = {
|
|
3409
|
+
name: "@hasna/testers",
|
|
3410
|
+
version: "0.0.10",
|
|
3411
|
+
description: "AI-powered QA testing CLI \u2014 spawns cheap AI agents to test web apps with headless browsers",
|
|
3412
|
+
type: "module",
|
|
3413
|
+
main: "dist/index.js",
|
|
3414
|
+
types: "dist/index.d.ts",
|
|
3415
|
+
bin: {
|
|
3416
|
+
testers: "dist/cli/index.js",
|
|
3417
|
+
"testers-mcp": "dist/mcp/index.js",
|
|
3418
|
+
"testers-serve": "dist/server/index.js"
|
|
3419
|
+
},
|
|
3420
|
+
exports: {
|
|
3421
|
+
".": {
|
|
3422
|
+
types: "./dist/index.d.ts",
|
|
3423
|
+
import: "./dist/index.js"
|
|
3424
|
+
}
|
|
3425
|
+
},
|
|
3426
|
+
files: [
|
|
3427
|
+
"dist/",
|
|
3428
|
+
"dashboard/dist/",
|
|
3429
|
+
"LICENSE",
|
|
3430
|
+
"README.md"
|
|
3431
|
+
],
|
|
3432
|
+
scripts: {
|
|
3433
|
+
build: "bun run build:dashboard && bun run build:cli && bun run build:mcp && bun run build:server && bun run build:lib && bun run build:types",
|
|
3434
|
+
"build:cli": "bun build src/cli/index.tsx --outdir dist/cli --target bun --external ink --external react --external chalk --external @modelcontextprotocol/sdk --external @anthropic-ai/sdk --external playwright",
|
|
3435
|
+
"build:mcp": "bun build src/mcp/index.ts --outdir dist/mcp --target bun --external @modelcontextprotocol/sdk --external @anthropic-ai/sdk --external playwright",
|
|
3436
|
+
"build:server": "bun build src/server/index.ts --outdir dist/server --target bun --external @anthropic-ai/sdk --external playwright",
|
|
3437
|
+
"build:lib": "bun build src/index.ts --outdir dist --target bun --external playwright --external @anthropic-ai/sdk --external @modelcontextprotocol/sdk",
|
|
3438
|
+
"build:types": "NODE_OPTIONS='--max-old-space-size=8192' tsc --emitDeclarationOnly --outDir dist --skipLibCheck",
|
|
3439
|
+
"build:dashboard": "cd dashboard && bun run build",
|
|
3440
|
+
typecheck: "tsc --noEmit",
|
|
3441
|
+
test: "bun test",
|
|
3442
|
+
"dev:cli": "bun run src/cli/index.tsx",
|
|
3443
|
+
"dev:mcp": "bun run src/mcp/index.ts",
|
|
3444
|
+
"dev:serve": "bun run src/server/index.ts",
|
|
3445
|
+
prepublishOnly: "bun run build"
|
|
3446
|
+
},
|
|
3447
|
+
dependencies: {
|
|
3448
|
+
"@anthropic-ai/sdk": "^0.52.0",
|
|
3449
|
+
"@modelcontextprotocol/sdk": "^1.12.1",
|
|
3450
|
+
chalk: "^5.4.1",
|
|
3451
|
+
commander: "^13.1.0",
|
|
3452
|
+
ink: "^5.2.0",
|
|
3453
|
+
playwright: "^1.50.0",
|
|
3454
|
+
react: "^18.3.1",
|
|
3455
|
+
zod: "^3.24.2"
|
|
3456
|
+
},
|
|
3457
|
+
devDependencies: {
|
|
3458
|
+
"@types/bun": "latest",
|
|
3459
|
+
"@types/react": "^18.3.18",
|
|
3460
|
+
typescript: "^5.7.3"
|
|
3461
|
+
},
|
|
3462
|
+
engines: {
|
|
3463
|
+
bun: ">=1.0.0"
|
|
3464
|
+
},
|
|
3465
|
+
publishConfig: {
|
|
3466
|
+
access: "public",
|
|
3467
|
+
registry: "https://registry.npmjs.org/"
|
|
3468
|
+
},
|
|
3469
|
+
repository: {
|
|
3470
|
+
type: "git",
|
|
3471
|
+
url: "https://github.com/hasna/open-testers.git"
|
|
3472
|
+
},
|
|
3473
|
+
license: "MIT",
|
|
3474
|
+
keywords: [
|
|
3475
|
+
"testing",
|
|
3476
|
+
"qa",
|
|
3477
|
+
"ai",
|
|
3478
|
+
"playwright",
|
|
3479
|
+
"browser",
|
|
3480
|
+
"screenshot",
|
|
3481
|
+
"automation",
|
|
3482
|
+
"cli",
|
|
3483
|
+
"mcp"
|
|
3484
|
+
]
|
|
3485
|
+
};
|
|
3486
|
+
|
|
3386
3487
|
// src/cli/index.tsx
|
|
3387
3488
|
init_scenarios();
|
|
3388
3489
|
init_runs();
|
|
3389
|
-
import
|
|
3490
|
+
import { render, Box, Text, useInput, useApp } from "ink";
|
|
3491
|
+
import React, { useState } from "react";
|
|
3390
3492
|
import { readFileSync as readFileSync6, readdirSync, writeFileSync as writeFileSync3 } from "fs";
|
|
3493
|
+
import { createInterface } from "readline";
|
|
3391
3494
|
import { join as join6, resolve } from "path";
|
|
3392
3495
|
|
|
3393
3496
|
// src/db/results.ts
|
|
@@ -4595,6 +4698,38 @@ async function dispatchWebhooks(event, run, schedule) {
|
|
|
4595
4698
|
}
|
|
4596
4699
|
}
|
|
4597
4700
|
|
|
4701
|
+
// src/lib/logs-integration.ts
|
|
4702
|
+
async function pushFailedRunToLogs(run, failedResults, scenarios) {
|
|
4703
|
+
const logsUrl = process.env.LOGS_URL;
|
|
4704
|
+
if (!logsUrl)
|
|
4705
|
+
return;
|
|
4706
|
+
const scenarioMap = new Map(scenarios.map((s) => [s.id, s]));
|
|
4707
|
+
const entries = failedResults.map((result) => {
|
|
4708
|
+
const scenario = scenarioMap.get(result.scenarioId);
|
|
4709
|
+
return {
|
|
4710
|
+
level: "error",
|
|
4711
|
+
source: "sdk",
|
|
4712
|
+
service: "testers",
|
|
4713
|
+
message: `[testers] Scenario failed: ${scenario?.name ?? result.scenarioId}${result.error ? ` \u2014 ${result.error}` : ""}`,
|
|
4714
|
+
metadata: {
|
|
4715
|
+
run_id: run.id,
|
|
4716
|
+
scenario_id: result.scenarioId,
|
|
4717
|
+
scenario_name: scenario?.name,
|
|
4718
|
+
url: run.url,
|
|
4719
|
+
status: result.status,
|
|
4720
|
+
duration_ms: result.durationMs
|
|
4721
|
+
}
|
|
4722
|
+
};
|
|
4723
|
+
});
|
|
4724
|
+
try {
|
|
4725
|
+
await fetch(`${logsUrl.replace(/\/$/, "")}/api/logs`, {
|
|
4726
|
+
method: "POST",
|
|
4727
|
+
headers: { "Content-Type": "application/json" },
|
|
4728
|
+
body: JSON.stringify(entries)
|
|
4729
|
+
});
|
|
4730
|
+
} catch {}
|
|
4731
|
+
}
|
|
4732
|
+
|
|
4598
4733
|
// src/lib/runner.ts
|
|
4599
4734
|
var eventHandler = null;
|
|
4600
4735
|
function onRunEvent(handler) {
|
|
@@ -4607,7 +4742,7 @@ function emit(event) {
|
|
|
4607
4742
|
function withTimeout(promise, ms, label) {
|
|
4608
4743
|
return new Promise((resolve, reject) => {
|
|
4609
4744
|
const timer = setTimeout(() => {
|
|
4610
|
-
reject(new Error(`Scenario
|
|
4745
|
+
reject(new Error(`Scenario '${label}' timed out after ${ms}ms. Try: testers run --timeout ${ms * 2} or simplify the scenario steps.`));
|
|
4611
4746
|
}, ms);
|
|
4612
4747
|
promise.then((val) => {
|
|
4613
4748
|
clearTimeout(timer);
|
|
@@ -4740,6 +4875,7 @@ async function runBatch(scenarios, options) {
|
|
|
4740
4875
|
} catch {}
|
|
4741
4876
|
return true;
|
|
4742
4877
|
};
|
|
4878
|
+
const maxRetries = options.retry ?? 0;
|
|
4743
4879
|
if (parallel <= 1) {
|
|
4744
4880
|
for (const scenario of sortedScenarios) {
|
|
4745
4881
|
if (!await canRun(scenario)) {
|
|
@@ -4750,7 +4886,13 @@ async function runBatch(scenarios, options) {
|
|
|
4750
4886
|
emit({ type: "scenario:error", scenarioId: scenario.id, scenarioName: scenario.name, error: "Dependency failed \u2014 skipped", runId: run.id });
|
|
4751
4887
|
continue;
|
|
4752
4888
|
}
|
|
4753
|
-
|
|
4889
|
+
let result = await runSingleScenario(scenario, run.id, options);
|
|
4890
|
+
let attempt = 1;
|
|
4891
|
+
while ((result.status === "failed" || result.status === "error") && attempt <= maxRetries) {
|
|
4892
|
+
emit({ type: "scenario:start", scenarioId: scenario.id, scenarioName: scenario.name, runId: run.id, retryAttempt: attempt + 1, maxRetries: maxRetries + 1 });
|
|
4893
|
+
result = await runSingleScenario(scenario, run.id, options);
|
|
4894
|
+
attempt++;
|
|
4895
|
+
}
|
|
4754
4896
|
results.push(result);
|
|
4755
4897
|
if (result.status === "failed" || result.status === "error") {
|
|
4756
4898
|
failedScenarioIds.add(scenario.id);
|
|
@@ -4797,6 +4939,10 @@ async function runBatch(scenarios, options) {
|
|
|
4797
4939
|
emit({ type: "run:complete", runId: run.id });
|
|
4798
4940
|
const eventType = finalRun.status === "failed" ? "failed" : "completed";
|
|
4799
4941
|
dispatchWebhooks(eventType, finalRun).catch(() => {});
|
|
4942
|
+
if (finalRun.status === "failed") {
|
|
4943
|
+
const failedResults = results.filter((r) => r.status === "failed" || r.status === "error");
|
|
4944
|
+
pushFailedRunToLogs(finalRun, failedResults, scenarios).catch(() => {});
|
|
4945
|
+
}
|
|
4800
4946
|
return { run: finalRun, results };
|
|
4801
4947
|
}
|
|
4802
4948
|
async function runByFilter(options) {
|
|
@@ -4912,6 +5058,10 @@ function estimateCost(model, tokens) {
|
|
|
4912
5058
|
// src/lib/reporter.ts
|
|
4913
5059
|
import chalk from "chalk";
|
|
4914
5060
|
init_scenarios();
|
|
5061
|
+
init_database();
|
|
5062
|
+
function useEmoji() {
|
|
5063
|
+
return !process.env["NO_COLOR"] && process.argv.indexOf("--no-color") === -1;
|
|
5064
|
+
}
|
|
4915
5065
|
function formatTerminal(run, results) {
|
|
4916
5066
|
const lines = [];
|
|
4917
5067
|
lines.push("");
|
|
@@ -4926,21 +5076,22 @@ function formatTerminal(run, results) {
|
|
|
4926
5076
|
const screenshotCount = screenshots.length;
|
|
4927
5077
|
let statusIcon;
|
|
4928
5078
|
let statusColor;
|
|
5079
|
+
const emoji = useEmoji();
|
|
4929
5080
|
switch (result.status) {
|
|
4930
5081
|
case "passed":
|
|
4931
|
-
statusIcon = chalk.green("PASS");
|
|
5082
|
+
statusIcon = emoji ? "\u2705" : chalk.green("PASS");
|
|
4932
5083
|
statusColor = chalk.green;
|
|
4933
5084
|
break;
|
|
4934
5085
|
case "failed":
|
|
4935
|
-
statusIcon = chalk.red("FAIL");
|
|
5086
|
+
statusIcon = emoji ? "\u274C" : chalk.red("FAIL");
|
|
4936
5087
|
statusColor = chalk.red;
|
|
4937
5088
|
break;
|
|
4938
5089
|
case "error":
|
|
4939
|
-
statusIcon = chalk.yellow("ERR ");
|
|
5090
|
+
statusIcon = emoji ? "\u26A0\uFE0F " : chalk.yellow("ERR ");
|
|
4940
5091
|
statusColor = chalk.yellow;
|
|
4941
5092
|
break;
|
|
4942
5093
|
default:
|
|
4943
|
-
statusIcon = chalk.dim("SKIP");
|
|
5094
|
+
statusIcon = emoji ? "\u23ED\uFE0F " : chalk.dim("SKIP");
|
|
4944
5095
|
statusColor = chalk.dim;
|
|
4945
5096
|
break;
|
|
4946
5097
|
}
|
|
@@ -4953,17 +5104,34 @@ function formatTerminal(run, results) {
|
|
|
4953
5104
|
}
|
|
4954
5105
|
}
|
|
4955
5106
|
lines.push("");
|
|
4956
|
-
lines.push(
|
|
5107
|
+
lines.push(formatActionableSummary(run, results));
|
|
4957
5108
|
lines.push("");
|
|
4958
5109
|
return lines.join(`
|
|
4959
5110
|
`);
|
|
4960
5111
|
}
|
|
4961
|
-
function
|
|
4962
|
-
const
|
|
4963
|
-
const
|
|
4964
|
-
const
|
|
4965
|
-
const
|
|
4966
|
-
|
|
5112
|
+
function formatActionableSummary(run, results) {
|
|
5113
|
+
const emoji = useEmoji();
|
|
5114
|
+
const passedCount = results.filter((r) => r.status === "passed").length;
|
|
5115
|
+
const failedCount = results.filter((r) => r.status === "failed" || r.status === "error").length;
|
|
5116
|
+
const shortId = run.id.slice(0, 8);
|
|
5117
|
+
const passStr = `${emoji ? "\u2705" : "PASS"} ${passedCount} passed`;
|
|
5118
|
+
const failStr = failedCount > 0 ? ` ${emoji ? "\u274C" : "FAIL"} ${failedCount} failed` : "";
|
|
5119
|
+
const lines = [];
|
|
5120
|
+
lines.push(` ${chalk.bold(passStr)}${failedCount > 0 ? chalk.bold(failStr) : ""}`);
|
|
5121
|
+
if (failedCount > 0) {
|
|
5122
|
+
lines.push(chalk.dim(` retry failed: testers retry ${shortId} | view: testers results ${shortId}`));
|
|
5123
|
+
} else {
|
|
5124
|
+
lines.push(chalk.dim(` view: testers results ${shortId}`));
|
|
5125
|
+
}
|
|
5126
|
+
const totalCostCents = results.reduce((sum, r) => sum + (r.costCents ?? 0), 0);
|
|
5127
|
+
const totalTokens = results.reduce((sum, r) => sum + (r.tokensUsed ?? 0), 0);
|
|
5128
|
+
if (totalTokens > 0) {
|
|
5129
|
+
const costStr = `$${(totalCostCents / 100).toFixed(4)}`;
|
|
5130
|
+
const tokensStr = totalTokens.toLocaleString();
|
|
5131
|
+
lines.push(chalk.dim(` ${emoji ? "\uD83D\uDCB0" : "cost:"} Cost: ${costStr} (${tokensStr} tokens)`));
|
|
5132
|
+
}
|
|
5133
|
+
return lines.join(`
|
|
5134
|
+
`);
|
|
4967
5135
|
}
|
|
4968
5136
|
function formatJSON(run, results) {
|
|
4969
5137
|
const output = {
|
|
@@ -5043,6 +5211,15 @@ function formatRunList(runs) {
|
|
|
5043
5211
|
return lines.join(`
|
|
5044
5212
|
`);
|
|
5045
5213
|
}
|
|
5214
|
+
function getScenarioRunStats(scenarioId) {
|
|
5215
|
+
const db2 = getDatabase();
|
|
5216
|
+
const lastRow = db2.query("SELECT status FROM results WHERE scenario_id = ? ORDER BY created_at DESC LIMIT 1").get(scenarioId);
|
|
5217
|
+
const statsRow = db2.query("SELECT COUNT(*) as total, SUM(CASE WHEN status = 'passed' THEN 1 ELSE 0 END) as passed FROM results WHERE scenario_id = ?").get(scenarioId);
|
|
5218
|
+
return {
|
|
5219
|
+
lastStatus: lastRow ? lastRow.status : null,
|
|
5220
|
+
passRate: statsRow && statsRow.total > 0 ? `${statsRow.passed}/${statsRow.total}` : "\u2014"
|
|
5221
|
+
};
|
|
5222
|
+
}
|
|
5046
5223
|
function formatScenarioList(scenarios) {
|
|
5047
5224
|
const lines = [];
|
|
5048
5225
|
lines.push("");
|
|
@@ -5057,7 +5234,21 @@ function formatScenarioList(scenarios) {
|
|
|
5057
5234
|
for (const s of scenarios) {
|
|
5058
5235
|
const priorityColor = s.priority === "critical" ? chalk.red : s.priority === "high" ? chalk.yellow : s.priority === "medium" ? chalk.blue : chalk.dim;
|
|
5059
5236
|
const tags = s.tags.length > 0 ? chalk.dim(` [${s.tags.join(", ")}]`) : "";
|
|
5060
|
-
|
|
5237
|
+
let lastStatusIcon = chalk.dim("\u2014");
|
|
5238
|
+
let passRateStr = chalk.dim("\u2014");
|
|
5239
|
+
if (s.id) {
|
|
5240
|
+
const stats = getScenarioRunStats(s.id);
|
|
5241
|
+
if (stats.lastStatus === "passed")
|
|
5242
|
+
lastStatusIcon = chalk.green("\u2713");
|
|
5243
|
+
else if (stats.lastStatus === "failed")
|
|
5244
|
+
lastStatusIcon = chalk.red("\u2717");
|
|
5245
|
+
else if (stats.lastStatus === "error")
|
|
5246
|
+
lastStatusIcon = chalk.yellow("!");
|
|
5247
|
+
else if (stats.lastStatus === "skipped")
|
|
5248
|
+
lastStatusIcon = chalk.dim("~");
|
|
5249
|
+
passRateStr = stats.passRate === "\u2014" ? chalk.dim("\u2014") : chalk.dim(stats.passRate);
|
|
5250
|
+
}
|
|
5251
|
+
lines.push(` ${chalk.cyan(s.shortId)} ${s.name} ${priorityColor(s.priority)}${tags} ${lastStatusIcon} ${passRateStr}`);
|
|
5061
5252
|
}
|
|
5062
5253
|
lines.push("");
|
|
5063
5254
|
return lines.join(`
|
|
@@ -5251,26 +5442,146 @@ function detectFramework(dir) {
|
|
|
5251
5442
|
return null;
|
|
5252
5443
|
}
|
|
5253
5444
|
function getStarterScenarios(framework, projectId) {
|
|
5445
|
+
if (framework.name === "Next.js") {
|
|
5446
|
+
const scenarios2 = [
|
|
5447
|
+
{
|
|
5448
|
+
name: "Homepage loads",
|
|
5449
|
+
description: "Navigate to the homepage and verify it loads correctly. Check that the main heading and content are visible, and there are no console errors.",
|
|
5450
|
+
tags: ["smoke"],
|
|
5451
|
+
priority: "high",
|
|
5452
|
+
projectId
|
|
5453
|
+
},
|
|
5454
|
+
{
|
|
5455
|
+
name: "404 page works",
|
|
5456
|
+
description: "Navigate to a non-existent URL (e.g. /this-page-does-not-exist) and verify the Next.js 404 page renders correctly.",
|
|
5457
|
+
tags: ["smoke"],
|
|
5458
|
+
priority: "medium",
|
|
5459
|
+
projectId
|
|
5460
|
+
},
|
|
5461
|
+
{
|
|
5462
|
+
name: "Navigation links work",
|
|
5463
|
+
description: "Click through the main navigation links and verify each page loads without errors. Check that client-side routing is working correctly.",
|
|
5464
|
+
tags: ["smoke"],
|
|
5465
|
+
priority: "medium",
|
|
5466
|
+
projectId
|
|
5467
|
+
}
|
|
5468
|
+
];
|
|
5469
|
+
if (framework.features.includes("hasAuth")) {
|
|
5470
|
+
scenarios2.push({
|
|
5471
|
+
name: "Login flow",
|
|
5472
|
+
description: "Navigate to the login page, enter valid credentials, and verify successful authentication and redirect.",
|
|
5473
|
+
tags: ["auth"],
|
|
5474
|
+
priority: "critical",
|
|
5475
|
+
projectId
|
|
5476
|
+
}, {
|
|
5477
|
+
name: "Protected route redirect",
|
|
5478
|
+
description: "Try to access a protected route without authentication and verify you are redirected to the login page.",
|
|
5479
|
+
tags: ["auth"],
|
|
5480
|
+
priority: "high",
|
|
5481
|
+
projectId
|
|
5482
|
+
});
|
|
5483
|
+
}
|
|
5484
|
+
if (framework.features.includes("hasForms")) {
|
|
5485
|
+
scenarios2.push({
|
|
5486
|
+
name: "Form validation",
|
|
5487
|
+
description: "Submit forms with empty/invalid data and verify validation errors appear correctly.",
|
|
5488
|
+
tags: ["forms"],
|
|
5489
|
+
priority: "medium",
|
|
5490
|
+
projectId
|
|
5491
|
+
});
|
|
5492
|
+
}
|
|
5493
|
+
return scenarios2;
|
|
5494
|
+
}
|
|
5495
|
+
if (framework.name === "Vite" || framework.name === "SvelteKit") {
|
|
5496
|
+
const scenarios2 = [
|
|
5497
|
+
{
|
|
5498
|
+
name: "Homepage loads",
|
|
5499
|
+
description: "Navigate to the homepage and verify it loads correctly with no console errors.",
|
|
5500
|
+
tags: ["smoke"],
|
|
5501
|
+
priority: "high",
|
|
5502
|
+
projectId
|
|
5503
|
+
},
|
|
5504
|
+
{
|
|
5505
|
+
name: "Mobile viewport check",
|
|
5506
|
+
description: "Set the viewport to 375x812 (iPhone) and verify the homepage renders correctly without horizontal scrolling or layout issues.",
|
|
5507
|
+
tags: ["responsive"],
|
|
5508
|
+
priority: "medium",
|
|
5509
|
+
projectId
|
|
5510
|
+
},
|
|
5511
|
+
{
|
|
5512
|
+
name: "No console errors",
|
|
5513
|
+
description: "Navigate through the app and verify there are no JavaScript errors or warnings in the browser console.",
|
|
5514
|
+
tags: ["smoke"],
|
|
5515
|
+
priority: "high",
|
|
5516
|
+
projectId
|
|
5517
|
+
}
|
|
5518
|
+
];
|
|
5519
|
+
if (framework.features.includes("hasAuth")) {
|
|
5520
|
+
scenarios2.push({
|
|
5521
|
+
name: "Login flow",
|
|
5522
|
+
description: "Navigate to the login page, enter valid credentials, and verify successful authentication.",
|
|
5523
|
+
tags: ["auth"],
|
|
5524
|
+
priority: "critical",
|
|
5525
|
+
projectId
|
|
5526
|
+
});
|
|
5527
|
+
}
|
|
5528
|
+
return scenarios2;
|
|
5529
|
+
}
|
|
5530
|
+
if (framework.name === "Nuxt") {
|
|
5531
|
+
const scenarios2 = [
|
|
5532
|
+
{
|
|
5533
|
+
name: "Homepage loads",
|
|
5534
|
+
description: "Navigate to the homepage and verify it loads correctly. Check that the main heading and content are visible.",
|
|
5535
|
+
tags: ["smoke"],
|
|
5536
|
+
priority: "high",
|
|
5537
|
+
projectId
|
|
5538
|
+
},
|
|
5539
|
+
{
|
|
5540
|
+
name: "Navigation works",
|
|
5541
|
+
description: "Click through main navigation links and verify each page loads without errors.",
|
|
5542
|
+
tags: ["smoke"],
|
|
5543
|
+
priority: "medium",
|
|
5544
|
+
projectId
|
|
5545
|
+
},
|
|
5546
|
+
{
|
|
5547
|
+
name: "Mobile viewport check",
|
|
5548
|
+
description: "Set the viewport to 375x812 and verify the homepage renders correctly on mobile.",
|
|
5549
|
+
tags: ["responsive"],
|
|
5550
|
+
priority: "medium",
|
|
5551
|
+
projectId
|
|
5552
|
+
}
|
|
5553
|
+
];
|
|
5554
|
+
if (framework.features.includes("hasAuth")) {
|
|
5555
|
+
scenarios2.push({
|
|
5556
|
+
name: "Login flow",
|
|
5557
|
+
description: "Navigate to the login page, enter valid credentials, and verify successful authentication.",
|
|
5558
|
+
tags: ["auth"],
|
|
5559
|
+
priority: "critical",
|
|
5560
|
+
projectId
|
|
5561
|
+
});
|
|
5562
|
+
}
|
|
5563
|
+
return scenarios2;
|
|
5564
|
+
}
|
|
5254
5565
|
const scenarios = [
|
|
5255
5566
|
{
|
|
5256
|
-
name: "
|
|
5257
|
-
description: "Navigate to the
|
|
5567
|
+
name: "Homepage loads",
|
|
5568
|
+
description: "Navigate to the homepage and verify it loads correctly with no console errors. Check that the main heading, navigation, and primary CTA are visible.",
|
|
5258
5569
|
tags: ["smoke"],
|
|
5259
5570
|
priority: "high",
|
|
5260
5571
|
projectId
|
|
5261
5572
|
},
|
|
5262
5573
|
{
|
|
5263
|
-
name: "
|
|
5264
|
-
description: "
|
|
5574
|
+
name: "Form submit works",
|
|
5575
|
+
description: "Find the main form on the page, fill it in with valid test data, submit it, and verify the success state.",
|
|
5265
5576
|
tags: ["smoke"],
|
|
5266
5577
|
priority: "medium",
|
|
5267
5578
|
projectId
|
|
5268
5579
|
},
|
|
5269
5580
|
{
|
|
5270
|
-
name: "
|
|
5271
|
-
description: "
|
|
5272
|
-
tags: ["
|
|
5273
|
-
priority: "
|
|
5581
|
+
name: "Mobile viewport check",
|
|
5582
|
+
description: "Set the viewport to 375x812 (iPhone) and verify the homepage renders correctly without horizontal scrolling or layout issues.",
|
|
5583
|
+
tags: ["responsive"],
|
|
5584
|
+
priority: "medium",
|
|
5274
5585
|
projectId
|
|
5275
5586
|
}
|
|
5276
5587
|
];
|
|
@@ -6051,6 +6362,15 @@ function getPeriodDays(period) {
|
|
|
6051
6362
|
return 30;
|
|
6052
6363
|
}
|
|
6053
6364
|
}
|
|
6365
|
+
function loadBudgetConfig() {
|
|
6366
|
+
const config = loadConfig();
|
|
6367
|
+
const budget = config.budget;
|
|
6368
|
+
return {
|
|
6369
|
+
maxPerRunCents: budget?.maxPerRunCents ?? 50,
|
|
6370
|
+
maxPerDayCents: budget?.maxPerDayCents ?? 500,
|
|
6371
|
+
warnAtPercent: budget?.warnAtPercent ?? 0.8
|
|
6372
|
+
};
|
|
6373
|
+
}
|
|
6054
6374
|
function getCostSummary(options) {
|
|
6055
6375
|
const db2 = getDatabase();
|
|
6056
6376
|
const period = options?.period ?? "month";
|
|
@@ -6118,6 +6438,30 @@ function getCostSummary(options) {
|
|
|
6118
6438
|
estimatedMonthlyCents
|
|
6119
6439
|
};
|
|
6120
6440
|
}
|
|
6441
|
+
function checkBudget(estimatedCostCents) {
|
|
6442
|
+
const budget = loadBudgetConfig();
|
|
6443
|
+
if (estimatedCostCents > budget.maxPerRunCents) {
|
|
6444
|
+
return {
|
|
6445
|
+
allowed: false,
|
|
6446
|
+
warning: `Estimated cost (${formatDollars(estimatedCostCents)}) exceeds per-run limit (${formatDollars(budget.maxPerRunCents)})`
|
|
6447
|
+
};
|
|
6448
|
+
}
|
|
6449
|
+
const todaySummary = getCostSummary({ period: "day" });
|
|
6450
|
+
const projectedDaily = todaySummary.totalCostCents + estimatedCostCents;
|
|
6451
|
+
if (projectedDaily > budget.maxPerDayCents) {
|
|
6452
|
+
return {
|
|
6453
|
+
allowed: false,
|
|
6454
|
+
warning: `Daily spending (${formatDollars(todaySummary.totalCostCents)}) + this run (${formatDollars(estimatedCostCents)}) would exceed daily limit (${formatDollars(budget.maxPerDayCents)})`
|
|
6455
|
+
};
|
|
6456
|
+
}
|
|
6457
|
+
if (projectedDaily > budget.maxPerDayCents * budget.warnAtPercent) {
|
|
6458
|
+
return {
|
|
6459
|
+
allowed: true,
|
|
6460
|
+
warning: `Approaching daily limit: ${formatDollars(projectedDaily)} of ${formatDollars(budget.maxPerDayCents)} (${Math.round(projectedDaily / budget.maxPerDayCents * 100)}%)`
|
|
6461
|
+
};
|
|
6462
|
+
}
|
|
6463
|
+
return { allowed: true };
|
|
6464
|
+
}
|
|
6121
6465
|
function formatDollars(cents) {
|
|
6122
6466
|
return `$${(cents / 100).toFixed(2)}`;
|
|
6123
6467
|
}
|
|
@@ -6148,12 +6492,13 @@ function formatCostsTerminal(summary) {
|
|
|
6148
6492
|
}
|
|
6149
6493
|
if (summary.byScenario.length > 0) {
|
|
6150
6494
|
lines.push("");
|
|
6151
|
-
lines.push(chalk4.bold("
|
|
6152
|
-
lines.push(` ${"Scenario".padEnd(40)} ${"Cost".padEnd(12)} ${"
|
|
6153
|
-
lines.push(` ${"\u2500".repeat(40)} ${"\u2500".repeat(12)} ${"\u2500".repeat(12)} ${"\u2500".repeat(6)}`);
|
|
6495
|
+
lines.push(chalk4.bold(" Scenarios by Cost (most expensive first)"));
|
|
6496
|
+
lines.push(` ${"Scenario".padEnd(40)} ${"Total Cost".padEnd(12)} ${"Avg/Run".padEnd(12)} ${"Runs".padEnd(6)} Tokens`);
|
|
6497
|
+
lines.push(` ${"\u2500".repeat(40)} ${"\u2500".repeat(12)} ${"\u2500".repeat(12)} ${"\u2500".repeat(6)} ${"\u2500".repeat(10)}`);
|
|
6154
6498
|
for (const s of summary.byScenario) {
|
|
6155
6499
|
const label = s.name.length > 38 ? s.name.slice(0, 35) + "..." : s.name;
|
|
6156
|
-
|
|
6500
|
+
const avgPerRun = s.runs > 0 ? s.costCents / s.runs : 0;
|
|
6501
|
+
lines.push(` ${label.padEnd(40)} ${formatDollars(s.costCents).padEnd(12)} ${formatDollars(avgPerRun).padEnd(12)} ${String(s.runs).padEnd(6)} ${formatTokens(s.tokens)}`);
|
|
6157
6502
|
}
|
|
6158
6503
|
}
|
|
6159
6504
|
lines.push("");
|
|
@@ -6163,6 +6508,17 @@ function formatCostsTerminal(summary) {
|
|
|
6163
6508
|
function formatCostsJSON(summary) {
|
|
6164
6509
|
return JSON.stringify(summary, null, 2);
|
|
6165
6510
|
}
|
|
6511
|
+
function formatCostsCsv(summary) {
|
|
6512
|
+
const lines = [];
|
|
6513
|
+
lines.push("scenario,runs,total_cost_cents,avg_cost_cents,tokens");
|
|
6514
|
+
for (const s of summary.byScenario) {
|
|
6515
|
+
const avgCostCents = s.runs > 0 ? s.costCents / s.runs : 0;
|
|
6516
|
+
const name = s.name.includes(",") ? `"${s.name.replace(/"/g, '""')}"` : s.name;
|
|
6517
|
+
lines.push(`${name},${s.runs},${s.costCents},${avgCostCents.toFixed(2)},${s.tokens}`);
|
|
6518
|
+
}
|
|
6519
|
+
return lines.join(`
|
|
6520
|
+
`);
|
|
6521
|
+
}
|
|
6166
6522
|
|
|
6167
6523
|
// src/db/schedules.ts
|
|
6168
6524
|
init_database();
|
|
@@ -6505,6 +6861,241 @@ function parseAssertionString(str) {
|
|
|
6505
6861
|
|
|
6506
6862
|
// src/cli/index.tsx
|
|
6507
6863
|
import { existsSync as existsSync8, mkdirSync as mkdirSync4 } from "fs";
|
|
6864
|
+
import { jsxDEV } from "react/jsx-dev-runtime";
|
|
6865
|
+
var PRIORITIES = ["low", "medium", "high", "critical"];
|
|
6866
|
+
function AddForm({ onComplete }) {
|
|
6867
|
+
const { exit } = useApp();
|
|
6868
|
+
const [state, setState] = useState({
|
|
6869
|
+
name: "",
|
|
6870
|
+
url: "",
|
|
6871
|
+
description: "",
|
|
6872
|
+
priority: "medium",
|
|
6873
|
+
tags: "",
|
|
6874
|
+
field: "name",
|
|
6875
|
+
buffer: ""
|
|
6876
|
+
});
|
|
6877
|
+
useInput((input, key) => {
|
|
6878
|
+
if (key.escape) {
|
|
6879
|
+
onComplete(null);
|
|
6880
|
+
exit();
|
|
6881
|
+
return;
|
|
6882
|
+
}
|
|
6883
|
+
if (key.return) {
|
|
6884
|
+
if (state.field === "name") {
|
|
6885
|
+
const val = state.buffer.trim();
|
|
6886
|
+
if (!val)
|
|
6887
|
+
return;
|
|
6888
|
+
setState((s) => ({ ...s, name: val, buffer: "", field: "url" }));
|
|
6889
|
+
} else if (state.field === "url") {
|
|
6890
|
+
setState((s) => ({ ...s, url: s.buffer.trim(), buffer: "", field: "description" }));
|
|
6891
|
+
} else if (state.field === "description") {
|
|
6892
|
+
setState((s) => ({ ...s, description: s.buffer.trim(), buffer: "", field: "priority" }));
|
|
6893
|
+
} else if (state.field === "priority") {
|
|
6894
|
+
setState((s) => ({ ...s, buffer: "", field: "tags" }));
|
|
6895
|
+
} else if (state.field === "tags") {
|
|
6896
|
+
setState((s) => ({ ...s, tags: s.buffer.trim(), buffer: "", field: "confirm" }));
|
|
6897
|
+
} else if (state.field === "confirm") {
|
|
6898
|
+
onComplete(state);
|
|
6899
|
+
exit();
|
|
6900
|
+
}
|
|
6901
|
+
return;
|
|
6902
|
+
}
|
|
6903
|
+
if (key.backspace || key.delete) {
|
|
6904
|
+
if (state.field === "priority")
|
|
6905
|
+
return;
|
|
6906
|
+
setState((s) => ({ ...s, buffer: s.buffer.slice(0, -1) }));
|
|
6907
|
+
return;
|
|
6908
|
+
}
|
|
6909
|
+
if (state.field === "priority") {
|
|
6910
|
+
if (key.leftArrow || key.rightArrow) {
|
|
6911
|
+
const idx = PRIORITIES.indexOf(state.priority);
|
|
6912
|
+
const next = key.rightArrow ? PRIORITIES[(idx + 1) % PRIORITIES.length] : PRIORITIES[(idx - 1 + PRIORITIES.length) % PRIORITIES.length];
|
|
6913
|
+
setState((s) => ({ ...s, priority: next }));
|
|
6914
|
+
}
|
|
6915
|
+
return;
|
|
6916
|
+
}
|
|
6917
|
+
if (!key.ctrl && !key.meta && input) {
|
|
6918
|
+
setState((s) => ({ ...s, buffer: s.buffer + input }));
|
|
6919
|
+
}
|
|
6920
|
+
});
|
|
6921
|
+
const fieldLabel = (label, field, value, hint) => {
|
|
6922
|
+
const active = state.field === field;
|
|
6923
|
+
const displayValue = active ? state.buffer + (active ? "\u2588" : "") : value;
|
|
6924
|
+
return /* @__PURE__ */ jsxDEV(Box, {
|
|
6925
|
+
flexDirection: "row",
|
|
6926
|
+
gap: 1,
|
|
6927
|
+
children: [
|
|
6928
|
+
/* @__PURE__ */ jsxDEV(Text, {
|
|
6929
|
+
color: active ? "cyan" : "gray",
|
|
6930
|
+
children: active ? "\u2192" : " "
|
|
6931
|
+
}, undefined, false, undefined, this),
|
|
6932
|
+
/* @__PURE__ */ jsxDEV(Text, {
|
|
6933
|
+
color: active ? "white" : "gray",
|
|
6934
|
+
bold: active,
|
|
6935
|
+
children: [
|
|
6936
|
+
label,
|
|
6937
|
+
":"
|
|
6938
|
+
]
|
|
6939
|
+
}, undefined, true, undefined, this),
|
|
6940
|
+
/* @__PURE__ */ jsxDEV(Text, {
|
|
6941
|
+
color: active ? "white" : "gray",
|
|
6942
|
+
children: displayValue || (hint ? hint : "")
|
|
6943
|
+
}, undefined, false, undefined, this),
|
|
6944
|
+
!displayValue && hint && /* @__PURE__ */ jsxDEV(Text, {
|
|
6945
|
+
color: "gray",
|
|
6946
|
+
children: " "
|
|
6947
|
+
}, undefined, false, undefined, this)
|
|
6948
|
+
]
|
|
6949
|
+
}, field, true, undefined, this);
|
|
6950
|
+
};
|
|
6951
|
+
return /* @__PURE__ */ jsxDEV(Box, {
|
|
6952
|
+
flexDirection: "column",
|
|
6953
|
+
gap: 0,
|
|
6954
|
+
paddingY: 1,
|
|
6955
|
+
children: [
|
|
6956
|
+
/* @__PURE__ */ jsxDEV(Text, {
|
|
6957
|
+
bold: true,
|
|
6958
|
+
color: "cyan",
|
|
6959
|
+
children: " New Test Scenario"
|
|
6960
|
+
}, undefined, false, undefined, this),
|
|
6961
|
+
/* @__PURE__ */ jsxDEV(Text, {
|
|
6962
|
+
color: "gray",
|
|
6963
|
+
children: " \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"
|
|
6964
|
+
}, undefined, false, undefined, this),
|
|
6965
|
+
fieldLabel(" Name ", "name", state.name),
|
|
6966
|
+
fieldLabel(" URL ", "url", state.url, "(optional)"),
|
|
6967
|
+
fieldLabel(" Description", "description", state.description, "(optional)"),
|
|
6968
|
+
/* @__PURE__ */ jsxDEV(Box, {
|
|
6969
|
+
flexDirection: "row",
|
|
6970
|
+
gap: 1,
|
|
6971
|
+
children: [
|
|
6972
|
+
/* @__PURE__ */ jsxDEV(Text, {
|
|
6973
|
+
color: state.field === "priority" ? "cyan" : "gray",
|
|
6974
|
+
children: state.field === "priority" ? "\u2192" : " "
|
|
6975
|
+
}, undefined, false, undefined, this),
|
|
6976
|
+
/* @__PURE__ */ jsxDEV(Text, {
|
|
6977
|
+
color: state.field === "priority" ? "white" : "gray",
|
|
6978
|
+
bold: state.field === "priority",
|
|
6979
|
+
children: " Priority :"
|
|
6980
|
+
}, undefined, false, undefined, this),
|
|
6981
|
+
PRIORITIES.map((p) => /* @__PURE__ */ jsxDEV(Text, {
|
|
6982
|
+
color: p === state.priority ? "cyan" : "gray",
|
|
6983
|
+
bold: p === state.priority,
|
|
6984
|
+
children: p === state.priority ? `[${p}]` : ` ${p} `
|
|
6985
|
+
}, p, false, undefined, this)),
|
|
6986
|
+
state.field === "priority" && /* @__PURE__ */ jsxDEV(Text, {
|
|
6987
|
+
color: "gray",
|
|
6988
|
+
children: " \u2190 \u2192"
|
|
6989
|
+
}, undefined, false, undefined, this)
|
|
6990
|
+
]
|
|
6991
|
+
}, undefined, true, undefined, this),
|
|
6992
|
+
fieldLabel(" Tags ", "tags", state.tags, "comma-separated, optional"),
|
|
6993
|
+
state.field === "confirm" && /* @__PURE__ */ jsxDEV(Box, {
|
|
6994
|
+
flexDirection: "column",
|
|
6995
|
+
marginTop: 1,
|
|
6996
|
+
children: [
|
|
6997
|
+
/* @__PURE__ */ jsxDEV(Text, {
|
|
6998
|
+
color: "gray",
|
|
6999
|
+
children: " \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"
|
|
7000
|
+
}, undefined, false, undefined, this),
|
|
7001
|
+
/* @__PURE__ */ jsxDEV(Text, {
|
|
7002
|
+
bold: true,
|
|
7003
|
+
color: "white",
|
|
7004
|
+
children: " Preview:"
|
|
7005
|
+
}, undefined, false, undefined, this),
|
|
7006
|
+
/* @__PURE__ */ jsxDEV(Text, {
|
|
7007
|
+
color: "gray",
|
|
7008
|
+
children: [
|
|
7009
|
+
" name: ",
|
|
7010
|
+
/* @__PURE__ */ jsxDEV(Text, {
|
|
7011
|
+
color: "white",
|
|
7012
|
+
children: state.name
|
|
7013
|
+
}, undefined, false, undefined, this)
|
|
7014
|
+
]
|
|
7015
|
+
}, undefined, true, undefined, this),
|
|
7016
|
+
state.url && /* @__PURE__ */ jsxDEV(Text, {
|
|
7017
|
+
color: "gray",
|
|
7018
|
+
children: [
|
|
7019
|
+
" url: ",
|
|
7020
|
+
/* @__PURE__ */ jsxDEV(Text, {
|
|
7021
|
+
color: "white",
|
|
7022
|
+
children: state.url
|
|
7023
|
+
}, undefined, false, undefined, this)
|
|
7024
|
+
]
|
|
7025
|
+
}, undefined, true, undefined, this),
|
|
7026
|
+
state.description && /* @__PURE__ */ jsxDEV(Text, {
|
|
7027
|
+
color: "gray",
|
|
7028
|
+
children: [
|
|
7029
|
+
" description: ",
|
|
7030
|
+
/* @__PURE__ */ jsxDEV(Text, {
|
|
7031
|
+
color: "white",
|
|
7032
|
+
children: state.description
|
|
7033
|
+
}, undefined, false, undefined, this)
|
|
7034
|
+
]
|
|
7035
|
+
}, undefined, true, undefined, this),
|
|
7036
|
+
/* @__PURE__ */ jsxDEV(Text, {
|
|
7037
|
+
color: "gray",
|
|
7038
|
+
children: [
|
|
7039
|
+
" priority: ",
|
|
7040
|
+
/* @__PURE__ */ jsxDEV(Text, {
|
|
7041
|
+
color: "cyan",
|
|
7042
|
+
children: state.priority
|
|
7043
|
+
}, undefined, false, undefined, this)
|
|
7044
|
+
]
|
|
7045
|
+
}, undefined, true, undefined, this),
|
|
7046
|
+
state.tags && /* @__PURE__ */ jsxDEV(Text, {
|
|
7047
|
+
color: "gray",
|
|
7048
|
+
children: [
|
|
7049
|
+
" tags: ",
|
|
7050
|
+
/* @__PURE__ */ jsxDEV(Text, {
|
|
7051
|
+
color: "white",
|
|
7052
|
+
children: state.tags
|
|
7053
|
+
}, undefined, false, undefined, this)
|
|
7054
|
+
]
|
|
7055
|
+
}, undefined, true, undefined, this),
|
|
7056
|
+
/* @__PURE__ */ jsxDEV(Text, {
|
|
7057
|
+
children: " "
|
|
7058
|
+
}, undefined, false, undefined, this),
|
|
7059
|
+
/* @__PURE__ */ jsxDEV(Text, {
|
|
7060
|
+
color: "green",
|
|
7061
|
+
children: " Press Enter to save, Escape to cancel"
|
|
7062
|
+
}, undefined, false, undefined, this)
|
|
7063
|
+
]
|
|
7064
|
+
}, undefined, true, undefined, this),
|
|
7065
|
+
state.field !== "confirm" && /* @__PURE__ */ jsxDEV(Text, {
|
|
7066
|
+
color: "gray",
|
|
7067
|
+
dimColor: true,
|
|
7068
|
+
children: " Tab/Enter to advance \xB7 Escape to cancel"
|
|
7069
|
+
}, undefined, false, undefined, this)
|
|
7070
|
+
]
|
|
7071
|
+
}, undefined, true, undefined, this);
|
|
7072
|
+
}
|
|
7073
|
+
async function runInteractiveAdd(projectId) {
|
|
7074
|
+
let savedResult = null;
|
|
7075
|
+
const { waitUntilExit } = render(React.createElement(AddForm, {
|
|
7076
|
+
onComplete: (data) => {
|
|
7077
|
+
savedResult = data;
|
|
7078
|
+
}
|
|
7079
|
+
}));
|
|
7080
|
+
await waitUntilExit();
|
|
7081
|
+
if (savedResult) {
|
|
7082
|
+
const result = savedResult;
|
|
7083
|
+
const tags = result.tags ? result.tags.split(",").map((t) => t.trim()).filter(Boolean) : [];
|
|
7084
|
+
const scenario = createScenario({
|
|
7085
|
+
name: result.name,
|
|
7086
|
+
description: result.description || result.name,
|
|
7087
|
+
steps: [],
|
|
7088
|
+
tags,
|
|
7089
|
+
priority: result.priority,
|
|
7090
|
+
projectId
|
|
7091
|
+
});
|
|
7092
|
+
log(chalk5.green(`
|
|
7093
|
+
Created scenario ${chalk5.bold(scenario.shortId)}: ${scenario.name}`));
|
|
7094
|
+
} else {
|
|
7095
|
+
log(chalk5.dim(`
|
|
7096
|
+
Cancelled.`));
|
|
7097
|
+
}
|
|
7098
|
+
}
|
|
6508
7099
|
function formatToolInput(input) {
|
|
6509
7100
|
const parts = [];
|
|
6510
7101
|
for (const [key, value] of Object.entries(input)) {
|
|
@@ -6515,7 +7106,19 @@ function formatToolInput(input) {
|
|
|
6515
7106
|
return parts.join(" ");
|
|
6516
7107
|
}
|
|
6517
7108
|
var program2 = new Command;
|
|
6518
|
-
|
|
7109
|
+
var QUIET = false;
|
|
7110
|
+
var NO_COLOR = false;
|
|
7111
|
+
function log(...args) {
|
|
7112
|
+
if (QUIET)
|
|
7113
|
+
return;
|
|
7114
|
+
console.log(...args);
|
|
7115
|
+
}
|
|
7116
|
+
function logError(...args) {
|
|
7117
|
+
if (QUIET)
|
|
7118
|
+
return;
|
|
7119
|
+
console.error(...args);
|
|
7120
|
+
}
|
|
7121
|
+
program2.name("testers").version(package_default.version).description("AI-powered browser testing CLI").option("-q, --quiet", "Suppress all output", false).option("--no-color", "Disable color output");
|
|
6519
7122
|
var CONFIG_DIR2 = join6(process.env["HOME"] ?? "~", ".testers");
|
|
6520
7123
|
var CONFIG_PATH2 = join6(CONFIG_DIR2, "config.json");
|
|
6521
7124
|
function getActiveProject() {
|
|
@@ -6530,7 +7133,7 @@ function getActiveProject() {
|
|
|
6530
7133
|
function resolveProject(optProject) {
|
|
6531
7134
|
return optProject ?? getActiveProject();
|
|
6532
7135
|
}
|
|
6533
|
-
program2.command("add
|
|
7136
|
+
program2.command("add [name]").alias("create").description("Create a new test scenario (interactive if no name/flags given)").option("-d, --description <text>", "Scenario description", "").option("-s, --steps <step>", "Test step (repeatable)", (val, acc) => {
|
|
6534
7137
|
acc.push(val);
|
|
6535
7138
|
return acc;
|
|
6536
7139
|
}, []).option("-t, --tag <tag>", "Tag (repeatable)", (val, acc) => {
|
|
@@ -6539,18 +7142,28 @@ program2.command("add <name>").description("Create a new test scenario").option(
|
|
|
6539
7142
|
}, []).option("-p, --priority <level>", "Priority level", "medium").option("-m, --model <model>", "AI model to use").option("--path <path>", "Target path on the URL").option("--auth", "Requires authentication", false).option("--timeout <ms>", "Timeout in milliseconds").option("--project <id>", "Project ID").option("--template <name>", "Seed scenarios from a template (auth, crud, forms, nav, a11y)").option("--assert <assertion>", "Structured assertion (repeatable). Formats: selector:<sel> visible, text:<sel> contains:<text>, no-console-errors, url:contains:<path>, title:contains:<text>, count:<sel> eq:<n>", (val, acc) => {
|
|
6540
7143
|
acc.push(val);
|
|
6541
7144
|
return acc;
|
|
6542
|
-
}, []).action((name, opts) => {
|
|
7145
|
+
}, []).action(async (name, opts) => {
|
|
6543
7146
|
try {
|
|
7147
|
+
const hasFlags = opts.description || opts.steps?.length || opts.tag?.length || opts.model || opts.path || opts.auth || opts.timeout || opts.template || opts.assert?.length;
|
|
7148
|
+
if (!name && !hasFlags) {
|
|
7149
|
+
const projectId2 = resolveProject(opts.project);
|
|
7150
|
+
await runInteractiveAdd(projectId2);
|
|
7151
|
+
return;
|
|
7152
|
+
}
|
|
7153
|
+
if (!name) {
|
|
7154
|
+
logError(chalk5.red("Error: scenario name is required"));
|
|
7155
|
+
process.exit(1);
|
|
7156
|
+
}
|
|
6544
7157
|
if (opts.template) {
|
|
6545
7158
|
const template = getTemplate(opts.template);
|
|
6546
7159
|
if (!template) {
|
|
6547
|
-
|
|
7160
|
+
logError(chalk5.red(`Unknown template: ${opts.template}. Available: ${listTemplateNames().join(", ")}`));
|
|
6548
7161
|
process.exit(1);
|
|
6549
7162
|
}
|
|
6550
7163
|
const projectId2 = resolveProject(opts.project);
|
|
6551
7164
|
for (const input of template) {
|
|
6552
7165
|
const s = createScenario({ ...input, projectId: projectId2 });
|
|
6553
|
-
|
|
7166
|
+
log(chalk5.green(` Created ${s.shortId}: ${s.name}`));
|
|
6554
7167
|
}
|
|
6555
7168
|
return;
|
|
6556
7169
|
}
|
|
@@ -6569,57 +7182,66 @@ program2.command("add <name>").description("Create a new test scenario").option(
|
|
|
6569
7182
|
assertions: assertions.length > 0 ? assertions : undefined,
|
|
6570
7183
|
projectId
|
|
6571
7184
|
});
|
|
6572
|
-
|
|
7185
|
+
log(chalk5.green(`Created scenario ${chalk5.bold(scenario.shortId)}: ${scenario.name}`));
|
|
6573
7186
|
} catch (error) {
|
|
6574
|
-
|
|
7187
|
+
logError(chalk5.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
6575
7188
|
process.exit(1);
|
|
6576
7189
|
}
|
|
6577
7190
|
});
|
|
6578
|
-
program2.command("list").description("List test scenarios").option("-t, --tag <tag>", "Filter by tag").option("-p, --priority <level>", "Filter by priority").option("--project <id>", "Filter by project ID").option("-l, --limit <n>", "Limit results", "50").action((opts) => {
|
|
7191
|
+
program2.command("list").alias("ls").description("List test scenarios").option("-t, --tag <tag>", "Filter by tag").option("-p, --priority <level>", "Filter by priority").option("--project <id>", "Filter by project ID").option("-l, --limit <n>", "Limit results", "50").option("--offset <n>", "Skip first N results", "0").option("--json", "Output as JSON", false).action((opts) => {
|
|
6579
7192
|
try {
|
|
6580
7193
|
const scenarios = listScenarios({
|
|
6581
7194
|
tags: opts.tag ? [opts.tag] : undefined,
|
|
6582
7195
|
priority: opts.priority,
|
|
6583
7196
|
projectId: opts.project,
|
|
6584
|
-
limit: parseInt(opts.limit, 10)
|
|
7197
|
+
limit: parseInt(opts.limit, 10),
|
|
7198
|
+
offset: parseInt(opts.offset, 10) || undefined
|
|
6585
7199
|
});
|
|
6586
|
-
|
|
7200
|
+
if (opts.json) {
|
|
7201
|
+
log(JSON.stringify(scenarios, null, 2));
|
|
7202
|
+
} else {
|
|
7203
|
+
log(formatScenarioList(scenarios));
|
|
7204
|
+
}
|
|
6587
7205
|
} catch (error) {
|
|
6588
|
-
|
|
7206
|
+
logError(chalk5.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
6589
7207
|
process.exit(1);
|
|
6590
7208
|
}
|
|
6591
7209
|
});
|
|
6592
|
-
program2.command("show <id>").description("Show scenario details").action((id) => {
|
|
7210
|
+
program2.command("show <id>").description("Show scenario details").option("--json", "Output as JSON", false).action((id, opts) => {
|
|
6593
7211
|
try {
|
|
6594
7212
|
const scenario = getScenario(id) ?? getScenarioByShortId(id);
|
|
6595
7213
|
if (!scenario) {
|
|
6596
|
-
|
|
7214
|
+
logError(chalk5.red(`Scenario not found: ${id}`));
|
|
6597
7215
|
process.exit(1);
|
|
6598
7216
|
}
|
|
6599
|
-
|
|
6600
|
-
|
|
6601
|
-
|
|
6602
|
-
|
|
6603
|
-
|
|
6604
|
-
|
|
6605
|
-
|
|
6606
|
-
|
|
6607
|
-
|
|
6608
|
-
|
|
6609
|
-
|
|
6610
|
-
|
|
6611
|
-
|
|
6612
|
-
|
|
7217
|
+
if (opts.json) {
|
|
7218
|
+
log(JSON.stringify(scenario, null, 2));
|
|
7219
|
+
return;
|
|
7220
|
+
}
|
|
7221
|
+
log("");
|
|
7222
|
+
log(chalk5.bold(` Scenario ${scenario.shortId}`));
|
|
7223
|
+
log(` Name: ${scenario.name}`);
|
|
7224
|
+
log(` ID: ${chalk5.dim(scenario.id)}`);
|
|
7225
|
+
log(` Description: ${scenario.description}`);
|
|
7226
|
+
log(` Priority: ${scenario.priority}`);
|
|
7227
|
+
log(` Model: ${scenario.model ?? chalk5.dim("default")}`);
|
|
7228
|
+
log(` Tags: ${scenario.tags.length > 0 ? scenario.tags.join(", ") : chalk5.dim("none")}`);
|
|
7229
|
+
log(` Path: ${scenario.targetPath ?? chalk5.dim("none")}`);
|
|
7230
|
+
log(` Auth: ${scenario.requiresAuth ? "yes" : "no"}`);
|
|
7231
|
+
log(` Timeout: ${scenario.timeoutMs ? `${scenario.timeoutMs}ms` : chalk5.dim("default")}`);
|
|
7232
|
+
log(` Version: ${scenario.version}`);
|
|
7233
|
+
log(` Created: ${scenario.createdAt}`);
|
|
7234
|
+
log(` Updated: ${scenario.updatedAt}`);
|
|
6613
7235
|
if (scenario.steps.length > 0) {
|
|
6614
|
-
|
|
6615
|
-
|
|
7236
|
+
log("");
|
|
7237
|
+
log(chalk5.bold(" Steps:"));
|
|
6616
7238
|
for (let i = 0;i < scenario.steps.length; i++) {
|
|
6617
|
-
|
|
7239
|
+
log(` ${i + 1}. ${scenario.steps[i]}`);
|
|
6618
7240
|
}
|
|
6619
7241
|
}
|
|
6620
|
-
|
|
7242
|
+
log("");
|
|
6621
7243
|
} catch (error) {
|
|
6622
|
-
|
|
7244
|
+
logError(chalk5.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
6623
7245
|
process.exit(1);
|
|
6624
7246
|
}
|
|
6625
7247
|
});
|
|
@@ -6633,7 +7255,7 @@ program2.command("update <id>").description("Update a scenario").option("-n, --n
|
|
|
6633
7255
|
try {
|
|
6634
7256
|
const scenario = getScenario(id) ?? getScenarioByShortId(id);
|
|
6635
7257
|
if (!scenario) {
|
|
6636
|
-
|
|
7258
|
+
logError(chalk5.red(`Scenario not found: ${id}`));
|
|
6637
7259
|
process.exit(1);
|
|
6638
7260
|
}
|
|
6639
7261
|
const updated = updateScenario(scenario.id, {
|
|
@@ -6644,42 +7266,62 @@ program2.command("update <id>").description("Update a scenario").option("-n, --n
|
|
|
6644
7266
|
priority: opts.priority,
|
|
6645
7267
|
model: opts.model
|
|
6646
7268
|
}, scenario.version);
|
|
6647
|
-
|
|
7269
|
+
log(chalk5.green(`Updated scenario ${chalk5.bold(updated.shortId)}: ${updated.name}`));
|
|
6648
7270
|
} catch (error) {
|
|
6649
|
-
|
|
7271
|
+
logError(chalk5.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
6650
7272
|
process.exit(1);
|
|
6651
7273
|
}
|
|
6652
7274
|
});
|
|
6653
|
-
program2.command("delete <id>").description("Delete a scenario").action((id) => {
|
|
7275
|
+
program2.command("delete <id>").description("Delete a scenario").option("-y, --yes", "Skip confirmation prompt", false).action(async (id, opts) => {
|
|
6654
7276
|
try {
|
|
6655
7277
|
const scenario = getScenario(id) ?? getScenarioByShortId(id);
|
|
6656
7278
|
if (!scenario) {
|
|
6657
|
-
|
|
7279
|
+
logError(chalk5.red(`Scenario not found: ${id}`));
|
|
6658
7280
|
process.exit(1);
|
|
6659
7281
|
}
|
|
7282
|
+
if (!opts.yes) {
|
|
7283
|
+
process.stdout.write(chalk5.yellow(`Delete scenario ${scenario.shortId} "${scenario.name}"? [y/N] `));
|
|
7284
|
+
const answer = await new Promise((resolve2) => {
|
|
7285
|
+
let buf = "";
|
|
7286
|
+
process.stdin.setRawMode?.(true);
|
|
7287
|
+
process.stdin.resume();
|
|
7288
|
+
process.stdin.once("data", (chunk) => {
|
|
7289
|
+
buf = chunk.toString().trim().toLowerCase();
|
|
7290
|
+
process.stdin.setRawMode?.(false);
|
|
7291
|
+
process.stdin.pause();
|
|
7292
|
+
process.stdout.write(`
|
|
7293
|
+
`);
|
|
7294
|
+
resolve2(buf);
|
|
7295
|
+
});
|
|
7296
|
+
});
|
|
7297
|
+
if (answer !== "y" && answer !== "yes") {
|
|
7298
|
+
log(chalk5.dim("Cancelled."));
|
|
7299
|
+
return;
|
|
7300
|
+
}
|
|
7301
|
+
}
|
|
6660
7302
|
const deleted = deleteScenario(scenario.id);
|
|
6661
7303
|
if (deleted) {
|
|
6662
|
-
|
|
7304
|
+
log(chalk5.green(`Deleted scenario ${scenario.shortId}: ${scenario.name}`));
|
|
6663
7305
|
} else {
|
|
6664
|
-
|
|
7306
|
+
logError(chalk5.red(`Failed to delete scenario: ${id}`));
|
|
6665
7307
|
process.exit(1);
|
|
6666
7308
|
}
|
|
6667
7309
|
} catch (error) {
|
|
6668
|
-
|
|
7310
|
+
logError(chalk5.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
6669
7311
|
process.exit(1);
|
|
6670
7312
|
}
|
|
6671
7313
|
});
|
|
6672
|
-
program2.command("run [url] [description]").description("Run test scenarios against a URL").option("-t, --tag <tag>", "Filter by tag (repeatable)", (val, acc) => {
|
|
7314
|
+
program2.command("run [url] [description]").alias("test").description("Run test scenarios against a URL").option("-t, --tag <tag>", "Filter by tag (repeatable)", (val, acc) => {
|
|
6673
7315
|
acc.push(val);
|
|
6674
7316
|
return acc;
|
|
6675
|
-
}, []).option("-s, --scenario <id>", "Run specific scenario ID").option("-p, --priority <level>", "Filter by priority").option("--headed", "Run browser in headed mode", false).option("-m, --model <model>", "AI model to use").option("--parallel <n>", "Number of parallel browsers", "1").option("--json", "Output results as JSON", false).option("-o, --output <filepath>", "Write JSON results to file").option("--timeout <ms>", "Timeout in milliseconds").option("--from-todos", "Import scenarios from todos before running", false).option("--project <id>", "Project ID").option("-b, --background", "Start run in background and return immediately", false).option("--browser <engine>", "Browser engine: playwright or lightpanda", "playwright").option("--env <name>", "Use a named environment for the URL").action(async (urlArg, description, opts) => {
|
|
7317
|
+
}, []).option("-s, --scenario <id>", "Run specific scenario ID").option("-p, --priority <level>", "Filter by priority").option("--headed", "Run browser in headed mode", false).option("-m, --model <model>", "AI model to use").option("--parallel <n>", "Number of parallel browsers", "1").option("--json", "Output results as JSON", false).option("-o, --output <filepath>", "Write JSON results to file").option("--timeout <ms>", "Timeout in milliseconds").option("--from-todos", "Import scenarios from todos before running", false).option("--project <id>", "Project ID").option("-b, --background", "Start run in background and return immediately", false).option("--browser <engine>", "Browser engine: playwright or lightpanda", "playwright").option("--env <name>", "Use a named environment for the URL").option("--dry-run", "Print what would run without launching browser", false).option("--retry <n>", "Retry failed scenarios up to n times", "0").action(async (urlArg, description, opts) => {
|
|
6676
7318
|
try {
|
|
6677
7319
|
const projectId = resolveProject(opts.project);
|
|
6678
7320
|
let url = urlArg;
|
|
6679
7321
|
if (!url && opts.env) {
|
|
6680
7322
|
const env = getEnvironment(opts.env);
|
|
6681
7323
|
if (!env) {
|
|
6682
|
-
|
|
7324
|
+
logError(chalk5.red(`Environment not found: ${opts.env}`));
|
|
6683
7325
|
process.exit(1);
|
|
6684
7326
|
}
|
|
6685
7327
|
url = env.url;
|
|
@@ -6688,16 +7330,76 @@ program2.command("run [url] [description]").description("Run test scenarios agai
|
|
|
6688
7330
|
const defaultEnv = getDefaultEnvironment();
|
|
6689
7331
|
if (defaultEnv) {
|
|
6690
7332
|
url = defaultEnv.url;
|
|
6691
|
-
|
|
7333
|
+
log(chalk5.dim(`Using default environment: ${defaultEnv.name} (${defaultEnv.url})`));
|
|
6692
7334
|
}
|
|
6693
7335
|
}
|
|
6694
7336
|
if (!url) {
|
|
6695
|
-
|
|
7337
|
+
logError(chalk5.red("No URL provided. Pass a URL argument, use --env <name>, or set a default environment with 'testers env use <name>'."));
|
|
6696
7338
|
process.exit(1);
|
|
6697
7339
|
}
|
|
7340
|
+
if (!opts.dryRun && !opts.background) {
|
|
7341
|
+
const budgetResult = checkBudget(0);
|
|
7342
|
+
if (budgetResult.warning) {
|
|
7343
|
+
log(chalk5.yellow(` \u26A0\uFE0F Budget warning: ${budgetResult.warning}`));
|
|
7344
|
+
if (!budgetResult.allowed) {
|
|
7345
|
+
if (!opts.yes) {
|
|
7346
|
+
log(chalk5.yellow(" Use --yes to run anyway, or check your budget config."));
|
|
7347
|
+
process.exit(1);
|
|
7348
|
+
}
|
|
7349
|
+
log(chalk5.yellow(" --yes passed, proceeding despite budget limit."));
|
|
7350
|
+
}
|
|
7351
|
+
}
|
|
7352
|
+
}
|
|
6698
7353
|
if (opts.fromTodos) {
|
|
6699
7354
|
const result = importFromTodos({ projectId });
|
|
6700
|
-
|
|
7355
|
+
log(chalk5.blue(`Imported ${result.imported} scenarios from todos (${result.skipped} skipped)`));
|
|
7356
|
+
}
|
|
7357
|
+
if (opts.dryRun) {
|
|
7358
|
+
const dryScenarios = listScenarios({
|
|
7359
|
+
tags: opts.tag.length > 0 ? opts.tag : undefined,
|
|
7360
|
+
projectId
|
|
7361
|
+
}).filter((s) => {
|
|
7362
|
+
if (opts.scenario && s.id !== opts.scenario && s.shortId !== opts.scenario)
|
|
7363
|
+
return false;
|
|
7364
|
+
if (opts.priority && s.priority !== opts.priority)
|
|
7365
|
+
return false;
|
|
7366
|
+
return true;
|
|
7367
|
+
});
|
|
7368
|
+
log("");
|
|
7369
|
+
log(chalk5.bold(" Dry Run \u2014 scenarios that would execute:"));
|
|
7370
|
+
log("");
|
|
7371
|
+
if (dryScenarios.length === 0) {
|
|
7372
|
+
log(chalk5.yellow(" No matching scenarios found."));
|
|
7373
|
+
} else {
|
|
7374
|
+
for (const s of dryScenarios) {
|
|
7375
|
+
const assertionErrors = [];
|
|
7376
|
+
for (const a of s.assertions ?? []) {
|
|
7377
|
+
try {
|
|
7378
|
+
parseAssertionString(a);
|
|
7379
|
+
} catch {
|
|
7380
|
+
assertionErrors.push(a);
|
|
7381
|
+
}
|
|
7382
|
+
}
|
|
7383
|
+
let authOk = true;
|
|
7384
|
+
if (s.authPreset) {
|
|
7385
|
+
const presets = listAuthPresets();
|
|
7386
|
+
authOk = presets.some((p) => p.name === s.authPreset);
|
|
7387
|
+
}
|
|
7388
|
+
const statusIcon = assertionErrors.length === 0 && authOk ? chalk5.green("\u2713") : chalk5.red("\u2717");
|
|
7389
|
+
log(` ${statusIcon} ${chalk5.cyan(s.shortId)} ${s.name} ${chalk5.dim(`[${s.tags.join(", ")}]`)}`);
|
|
7390
|
+
if (assertionErrors.length > 0) {
|
|
7391
|
+
log(chalk5.red(` Invalid assertions: ${assertionErrors.join(", ")}`));
|
|
7392
|
+
}
|
|
7393
|
+
if (!authOk) {
|
|
7394
|
+
log(chalk5.red(` Auth preset not found: ${s.authPreset}`));
|
|
7395
|
+
}
|
|
7396
|
+
}
|
|
7397
|
+
}
|
|
7398
|
+
log("");
|
|
7399
|
+
log(chalk5.dim(` URL: ${url}`));
|
|
7400
|
+
log(chalk5.dim(` Total: ${dryScenarios.length} scenarios`));
|
|
7401
|
+
log("");
|
|
7402
|
+
process.exit(0);
|
|
6701
7403
|
}
|
|
6702
7404
|
if (opts.background) {
|
|
6703
7405
|
if (description) {
|
|
@@ -6715,52 +7417,56 @@ program2.command("run [url] [description]").description("Run test scenarios agai
|
|
|
6715
7417
|
projectId,
|
|
6716
7418
|
engine: opts.browser
|
|
6717
7419
|
});
|
|
6718
|
-
|
|
6719
|
-
|
|
6720
|
-
|
|
6721
|
-
|
|
7420
|
+
log(chalk5.green(`Run started in background: ${chalk5.bold(runId.slice(0, 8))}`));
|
|
7421
|
+
log(chalk5.dim(` Scenarios: ${scenarioCount}`));
|
|
7422
|
+
log(chalk5.dim(` URL: ${url}`));
|
|
7423
|
+
log(chalk5.dim(` Check progress: testers results ${runId.slice(0, 8)}`));
|
|
6722
7424
|
process.exit(0);
|
|
6723
7425
|
}
|
|
6724
7426
|
if (!opts.json && !opts.output) {
|
|
6725
7427
|
onRunEvent((event) => {
|
|
6726
7428
|
switch (event.type) {
|
|
6727
7429
|
case "scenario:start":
|
|
6728
|
-
|
|
7430
|
+
if (event.retryAttempt) {
|
|
7431
|
+
log(chalk5.yellow(` [retry] Retrying scenario ${event.scenarioName ?? event.scenarioId} (attempt ${event.retryAttempt}/${event.maxRetries})...`));
|
|
7432
|
+
} else {
|
|
7433
|
+
log(chalk5.blue(` [start] ${event.scenarioName ?? event.scenarioId}`));
|
|
7434
|
+
}
|
|
6729
7435
|
break;
|
|
6730
7436
|
case "step:thinking":
|
|
6731
7437
|
if (event.thinking) {
|
|
6732
7438
|
const preview = event.thinking.length > 120 ? event.thinking.slice(0, 120) + "..." : event.thinking;
|
|
6733
|
-
|
|
7439
|
+
log(chalk5.dim(` [think] ${preview}`));
|
|
6734
7440
|
}
|
|
6735
7441
|
break;
|
|
6736
7442
|
case "step:tool_call":
|
|
6737
|
-
|
|
7443
|
+
log(chalk5.cyan(` [step ${event.stepNumber}] ${event.toolName}${event.toolInput ? ` ${formatToolInput(event.toolInput)}` : ""}`));
|
|
6738
7444
|
break;
|
|
6739
7445
|
case "step:tool_result":
|
|
6740
7446
|
if (event.toolName === "report_result") {
|
|
6741
|
-
|
|
7447
|
+
log(chalk5.bold(` [result] ${event.toolResult}`));
|
|
6742
7448
|
} else {
|
|
6743
7449
|
const resultPreview = (event.toolResult ?? "").length > 100 ? (event.toolResult ?? "").slice(0, 100) + "..." : event.toolResult ?? "";
|
|
6744
|
-
|
|
7450
|
+
log(chalk5.dim(` [done] ${resultPreview}`));
|
|
6745
7451
|
}
|
|
6746
7452
|
break;
|
|
6747
7453
|
case "screenshot:captured":
|
|
6748
|
-
|
|
7454
|
+
log(chalk5.dim(` [screenshot] ${event.screenshotPath}`));
|
|
6749
7455
|
break;
|
|
6750
7456
|
case "scenario:pass":
|
|
6751
|
-
|
|
7457
|
+
log(chalk5.green(` [PASS] ${event.scenarioName}`));
|
|
6752
7458
|
break;
|
|
6753
7459
|
case "scenario:fail":
|
|
6754
|
-
|
|
7460
|
+
log(chalk5.red(` [FAIL] ${event.scenarioName}`));
|
|
6755
7461
|
break;
|
|
6756
7462
|
case "scenario:error":
|
|
6757
|
-
|
|
7463
|
+
log(chalk5.yellow(` [ERR] ${event.scenarioName}: ${event.error}`));
|
|
6758
7464
|
break;
|
|
6759
7465
|
}
|
|
6760
7466
|
});
|
|
6761
|
-
|
|
6762
|
-
|
|
6763
|
-
|
|
7467
|
+
log("");
|
|
7468
|
+
log(chalk5.bold(` Running tests against ${url}`));
|
|
7469
|
+
log("");
|
|
6764
7470
|
}
|
|
6765
7471
|
if (description) {
|
|
6766
7472
|
const scenario = createScenario({
|
|
@@ -6776,6 +7482,7 @@ program2.command("run [url] [description]").description("Run test scenarios agai
|
|
|
6776
7482
|
headed: opts.headed,
|
|
6777
7483
|
parallel: parseInt(opts.parallel, 10),
|
|
6778
7484
|
timeout: opts.timeout ? parseInt(opts.timeout, 10) : undefined,
|
|
7485
|
+
retry: parseInt(opts.retry ?? "0", 10),
|
|
6779
7486
|
projectId,
|
|
6780
7487
|
engine: opts.browser
|
|
6781
7488
|
});
|
|
@@ -6783,16 +7490,22 @@ program2.command("run [url] [description]").description("Run test scenarios agai
|
|
|
6783
7490
|
const jsonOutput = formatJSON(run2, results2);
|
|
6784
7491
|
if (opts.output) {
|
|
6785
7492
|
writeFileSync3(opts.output, jsonOutput, "utf-8");
|
|
6786
|
-
|
|
7493
|
+
log(chalk5.green(`Results written to ${opts.output}`));
|
|
6787
7494
|
}
|
|
6788
7495
|
if (opts.json) {
|
|
6789
|
-
|
|
7496
|
+
log(jsonOutput);
|
|
6790
7497
|
}
|
|
6791
7498
|
} else {
|
|
6792
|
-
|
|
7499
|
+
log(formatTerminal(run2, results2));
|
|
6793
7500
|
}
|
|
6794
7501
|
process.exit(getExitCode(run2));
|
|
6795
7502
|
}
|
|
7503
|
+
const noFilters = !opts.scenario && opts.tag.length === 0 && !opts.priority;
|
|
7504
|
+
if (noFilters && !opts.json && !opts.output) {
|
|
7505
|
+
const allScenarios = listScenarios({ projectId });
|
|
7506
|
+
log(chalk5.bold(` Running all ${allScenarios.length} scenarios...`));
|
|
7507
|
+
log("");
|
|
7508
|
+
}
|
|
6796
7509
|
const { run, results } = await runByFilter({
|
|
6797
7510
|
url,
|
|
6798
7511
|
tags: opts.tag.length > 0 ? opts.tag : undefined,
|
|
@@ -6802,6 +7515,7 @@ program2.command("run [url] [description]").description("Run test scenarios agai
|
|
|
6802
7515
|
headed: opts.headed,
|
|
6803
7516
|
parallel: parseInt(opts.parallel, 10),
|
|
6804
7517
|
timeout: opts.timeout ? parseInt(opts.timeout, 10) : undefined,
|
|
7518
|
+
retry: parseInt(opts.retry ?? "0", 10),
|
|
6805
7519
|
projectId,
|
|
6806
7520
|
engine: opts.browser
|
|
6807
7521
|
});
|
|
@@ -6809,43 +7523,52 @@ program2.command("run [url] [description]").description("Run test scenarios agai
|
|
|
6809
7523
|
const jsonOutput = formatJSON(run, results);
|
|
6810
7524
|
if (opts.output) {
|
|
6811
7525
|
writeFileSync3(opts.output, jsonOutput, "utf-8");
|
|
6812
|
-
|
|
7526
|
+
log(chalk5.green(`Results written to ${opts.output}`));
|
|
6813
7527
|
}
|
|
6814
7528
|
if (opts.json) {
|
|
6815
|
-
|
|
7529
|
+
log(jsonOutput);
|
|
6816
7530
|
}
|
|
6817
7531
|
} else {
|
|
6818
|
-
|
|
7532
|
+
log(formatTerminal(run, results));
|
|
6819
7533
|
}
|
|
6820
7534
|
process.exit(getExitCode(run));
|
|
6821
7535
|
} catch (error) {
|
|
6822
|
-
|
|
7536
|
+
logError(chalk5.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
6823
7537
|
process.exit(1);
|
|
6824
7538
|
}
|
|
6825
7539
|
});
|
|
6826
|
-
program2.command("runs").description("List past test runs").option("--status <status>", "Filter by status").option("-l, --limit <n>", "Limit results", "20").action((opts) => {
|
|
7540
|
+
program2.command("runs").description("List past test runs").option("--status <status>", "Filter by status").option("-l, --limit <n>", "Limit results", "20").option("--offset <n>", "Skip first N results", "0").option("--json", "Output as JSON", false).action((opts) => {
|
|
6827
7541
|
try {
|
|
6828
7542
|
const runs = listRuns({
|
|
6829
7543
|
status: opts.status,
|
|
6830
|
-
limit: parseInt(opts.limit, 10)
|
|
7544
|
+
limit: parseInt(opts.limit, 10),
|
|
7545
|
+
offset: parseInt(opts.offset, 10) || undefined
|
|
6831
7546
|
});
|
|
6832
|
-
|
|
7547
|
+
if (opts.json) {
|
|
7548
|
+
log(JSON.stringify(runs, null, 2));
|
|
7549
|
+
} else {
|
|
7550
|
+
log(formatRunList(runs));
|
|
7551
|
+
}
|
|
6833
7552
|
} catch (error) {
|
|
6834
|
-
|
|
7553
|
+
logError(chalk5.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
6835
7554
|
process.exit(1);
|
|
6836
7555
|
}
|
|
6837
7556
|
});
|
|
6838
|
-
program2.command("results <run-id>").description("Show results for a test run").action((runId) => {
|
|
7557
|
+
program2.command("results <run-id>").description("Show results for a test run").option("--json", "Output as JSON", false).action((runId, opts) => {
|
|
6839
7558
|
try {
|
|
6840
7559
|
const run = getRun(runId);
|
|
6841
7560
|
if (!run) {
|
|
6842
|
-
|
|
7561
|
+
logError(chalk5.red(`Run not found: ${runId}`));
|
|
6843
7562
|
process.exit(1);
|
|
6844
7563
|
}
|
|
6845
7564
|
const results = getResultsByRun(run.id);
|
|
6846
|
-
|
|
7565
|
+
if (opts.json) {
|
|
7566
|
+
log(formatJSON(run, results));
|
|
7567
|
+
} else {
|
|
7568
|
+
log(formatTerminal(run, results));
|
|
7569
|
+
}
|
|
6847
7570
|
} catch (error) {
|
|
6848
|
-
|
|
7571
|
+
logError(chalk5.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
6849
7572
|
process.exit(1);
|
|
6850
7573
|
}
|
|
6851
7574
|
});
|
|
@@ -6855,43 +7578,43 @@ program2.command("screenshots <id>").description("List screenshots for a run or
|
|
|
6855
7578
|
if (run) {
|
|
6856
7579
|
const results = getResultsByRun(run.id);
|
|
6857
7580
|
let total = 0;
|
|
6858
|
-
|
|
6859
|
-
|
|
6860
|
-
|
|
7581
|
+
log("");
|
|
7582
|
+
log(chalk5.bold(` Screenshots for run ${run.id.slice(0, 8)}`));
|
|
7583
|
+
log("");
|
|
6861
7584
|
for (const result of results) {
|
|
6862
7585
|
const screenshots2 = listScreenshots(result.id);
|
|
6863
7586
|
if (screenshots2.length > 0) {
|
|
6864
7587
|
const scenario = getScenario(result.scenarioId);
|
|
6865
7588
|
const label = scenario ? `${scenario.shortId}: ${scenario.name}` : result.scenarioId.slice(0, 8);
|
|
6866
|
-
|
|
7589
|
+
log(chalk5.bold(` ${label}`));
|
|
6867
7590
|
for (const ss of screenshots2) {
|
|
6868
|
-
|
|
7591
|
+
log(` ${chalk5.dim(String(ss.stepNumber).padStart(3, "0"))} ${ss.action} \u2014 ${chalk5.dim(ss.filePath)}`);
|
|
6869
7592
|
total++;
|
|
6870
7593
|
}
|
|
6871
|
-
|
|
7594
|
+
log("");
|
|
6872
7595
|
}
|
|
6873
7596
|
}
|
|
6874
7597
|
if (total === 0) {
|
|
6875
|
-
|
|
6876
|
-
|
|
7598
|
+
log(chalk5.dim(" No screenshots found."));
|
|
7599
|
+
log("");
|
|
6877
7600
|
}
|
|
6878
7601
|
return;
|
|
6879
7602
|
}
|
|
6880
7603
|
const screenshots = listScreenshots(id);
|
|
6881
7604
|
if (screenshots.length > 0) {
|
|
6882
|
-
|
|
6883
|
-
|
|
6884
|
-
|
|
7605
|
+
log("");
|
|
7606
|
+
log(chalk5.bold(` Screenshots for result ${id.slice(0, 8)}`));
|
|
7607
|
+
log("");
|
|
6885
7608
|
for (const ss of screenshots) {
|
|
6886
|
-
|
|
7609
|
+
log(` ${chalk5.dim(String(ss.stepNumber).padStart(3, "0"))} ${ss.action} \u2014 ${chalk5.dim(ss.filePath)}`);
|
|
6887
7610
|
}
|
|
6888
|
-
|
|
7611
|
+
log("");
|
|
6889
7612
|
return;
|
|
6890
7613
|
}
|
|
6891
|
-
|
|
7614
|
+
logError(chalk5.red(`No screenshots found for: ${id}`));
|
|
6892
7615
|
process.exit(1);
|
|
6893
7616
|
} catch (error) {
|
|
6894
|
-
|
|
7617
|
+
logError(chalk5.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
6895
7618
|
process.exit(1);
|
|
6896
7619
|
}
|
|
6897
7620
|
});
|
|
@@ -6900,7 +7623,7 @@ program2.command("import <dir>").description("Import markdown test files as scen
|
|
|
6900
7623
|
const absDir = resolve(dir);
|
|
6901
7624
|
const files = readdirSync(absDir).filter((f) => f.endsWith(".md"));
|
|
6902
7625
|
if (files.length === 0) {
|
|
6903
|
-
|
|
7626
|
+
log(chalk5.dim("No .md files found in directory."));
|
|
6904
7627
|
return;
|
|
6905
7628
|
}
|
|
6906
7629
|
let imported = 0;
|
|
@@ -6930,22 +7653,22 @@ program2.command("import <dir>").description("Import markdown test files as scen
|
|
|
6930
7653
|
description: descriptionLines.join(" ") || name,
|
|
6931
7654
|
steps
|
|
6932
7655
|
});
|
|
6933
|
-
|
|
7656
|
+
log(chalk5.green(` Imported ${chalk5.bold(scenario.shortId)}: ${scenario.name}`));
|
|
6934
7657
|
imported++;
|
|
6935
7658
|
}
|
|
6936
|
-
|
|
6937
|
-
|
|
7659
|
+
log("");
|
|
7660
|
+
log(chalk5.green(`Imported ${imported} scenario(s) from ${absDir}`));
|
|
6938
7661
|
} catch (error) {
|
|
6939
|
-
|
|
7662
|
+
logError(chalk5.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
6940
7663
|
process.exit(1);
|
|
6941
7664
|
}
|
|
6942
7665
|
});
|
|
6943
7666
|
program2.command("config").description("Show current configuration").action(() => {
|
|
6944
7667
|
try {
|
|
6945
7668
|
const config = loadConfig();
|
|
6946
|
-
|
|
7669
|
+
log(JSON.stringify(config, null, 2));
|
|
6947
7670
|
} catch (error) {
|
|
6948
|
-
|
|
7671
|
+
logError(chalk5.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
6949
7672
|
process.exit(1);
|
|
6950
7673
|
}
|
|
6951
7674
|
});
|
|
@@ -6954,33 +7677,33 @@ program2.command("status").description("Show database and auth status").action((
|
|
|
6954
7677
|
const config = loadConfig();
|
|
6955
7678
|
const hasApiKey = !!config.anthropicApiKey || !!process.env["ANTHROPIC_API_KEY"];
|
|
6956
7679
|
const dbPath = join6(process.env["HOME"] ?? "~", ".testers", "testers.db");
|
|
6957
|
-
|
|
6958
|
-
|
|
6959
|
-
|
|
6960
|
-
|
|
6961
|
-
|
|
6962
|
-
|
|
6963
|
-
|
|
6964
|
-
|
|
7680
|
+
log("");
|
|
7681
|
+
log(chalk5.bold(" Open Testers Status"));
|
|
7682
|
+
log("");
|
|
7683
|
+
log(` ANTHROPIC_API_KEY: ${hasApiKey ? chalk5.green("set") : chalk5.red("not set")}`);
|
|
7684
|
+
log(` Database: ${dbPath}`);
|
|
7685
|
+
log(` Default model: ${config.defaultModel}`);
|
|
7686
|
+
log(` Screenshots dir: ${config.screenshots.dir}`);
|
|
7687
|
+
log("");
|
|
6965
7688
|
} catch (error) {
|
|
6966
|
-
|
|
7689
|
+
logError(chalk5.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
6967
7690
|
process.exit(1);
|
|
6968
7691
|
}
|
|
6969
7692
|
});
|
|
6970
7693
|
program2.command("install-browser").description("Install browser engine").option("--engine <engine>", "Engine to install: playwright, lightpanda, or all", "playwright").action(async (opts) => {
|
|
6971
7694
|
try {
|
|
6972
7695
|
if (opts.engine === "all" || opts.engine === "playwright") {
|
|
6973
|
-
|
|
7696
|
+
log(chalk5.blue("Installing Playwright Chromium..."));
|
|
6974
7697
|
await installBrowser("playwright");
|
|
6975
|
-
|
|
7698
|
+
log(chalk5.green("Playwright Chromium installed."));
|
|
6976
7699
|
}
|
|
6977
7700
|
if (opts.engine === "all" || opts.engine === "lightpanda") {
|
|
6978
|
-
|
|
7701
|
+
log(chalk5.blue("Installing Lightpanda..."));
|
|
6979
7702
|
await installBrowser("lightpanda");
|
|
6980
|
-
|
|
7703
|
+
log(chalk5.green("Lightpanda installed."));
|
|
6981
7704
|
}
|
|
6982
7705
|
} catch (error) {
|
|
6983
|
-
|
|
7706
|
+
logError(chalk5.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
6984
7707
|
process.exit(1);
|
|
6985
7708
|
}
|
|
6986
7709
|
});
|
|
@@ -6992,9 +7715,9 @@ projectCmd.command("create <name>").description("Create a new project").option("
|
|
|
6992
7715
|
path: opts.path,
|
|
6993
7716
|
description: opts.description
|
|
6994
7717
|
});
|
|
6995
|
-
|
|
7718
|
+
log(chalk5.green(`Created project ${chalk5.bold(project.name)} (${project.id})`));
|
|
6996
7719
|
} catch (error) {
|
|
6997
|
-
|
|
7720
|
+
logError(chalk5.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
6998
7721
|
process.exit(1);
|
|
6999
7722
|
}
|
|
7000
7723
|
});
|
|
@@ -7002,20 +7725,20 @@ projectCmd.command("list").description("List all projects").action(() => {
|
|
|
7002
7725
|
try {
|
|
7003
7726
|
const projects = listProjects();
|
|
7004
7727
|
if (projects.length === 0) {
|
|
7005
|
-
|
|
7728
|
+
log(chalk5.dim("No projects found."));
|
|
7006
7729
|
return;
|
|
7007
7730
|
}
|
|
7008
|
-
|
|
7009
|
-
|
|
7010
|
-
|
|
7011
|
-
|
|
7012
|
-
|
|
7731
|
+
log("");
|
|
7732
|
+
log(chalk5.bold(" Projects"));
|
|
7733
|
+
log("");
|
|
7734
|
+
log(` ${"ID".padEnd(38)} ${"Name".padEnd(24)} ${"Path".padEnd(30)} Created`);
|
|
7735
|
+
log(` ${"\u2500".repeat(38)} ${"\u2500".repeat(24)} ${"\u2500".repeat(30)} ${"\u2500".repeat(20)}`);
|
|
7013
7736
|
for (const p of projects) {
|
|
7014
|
-
|
|
7737
|
+
log(` ${p.id.padEnd(38)} ${p.name.padEnd(24)} ${(p.path ?? chalk5.dim("\u2014")).toString().padEnd(30)} ${p.createdAt}`);
|
|
7015
7738
|
}
|
|
7016
|
-
|
|
7739
|
+
log("");
|
|
7017
7740
|
} catch (error) {
|
|
7018
|
-
|
|
7741
|
+
logError(chalk5.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
7019
7742
|
process.exit(1);
|
|
7020
7743
|
}
|
|
7021
7744
|
});
|
|
@@ -7023,19 +7746,19 @@ projectCmd.command("show <id>").description("Show project details").action((id)
|
|
|
7023
7746
|
try {
|
|
7024
7747
|
const project = getProject(id);
|
|
7025
7748
|
if (!project) {
|
|
7026
|
-
|
|
7749
|
+
logError(chalk5.red(`Project not found: ${id}`));
|
|
7027
7750
|
process.exit(1);
|
|
7028
7751
|
}
|
|
7029
|
-
|
|
7030
|
-
|
|
7031
|
-
|
|
7032
|
-
|
|
7033
|
-
|
|
7034
|
-
|
|
7035
|
-
|
|
7036
|
-
|
|
7752
|
+
log("");
|
|
7753
|
+
log(chalk5.bold(` Project: ${project.name}`));
|
|
7754
|
+
log(` ID: ${project.id}`);
|
|
7755
|
+
log(` Path: ${project.path ?? chalk5.dim("none")}`);
|
|
7756
|
+
log(` Description: ${project.description ?? chalk5.dim("none")}`);
|
|
7757
|
+
log(` Created: ${project.createdAt}`);
|
|
7758
|
+
log(` Updated: ${project.updatedAt}`);
|
|
7759
|
+
log("");
|
|
7037
7760
|
} catch (error) {
|
|
7038
|
-
|
|
7761
|
+
logError(chalk5.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
7039
7762
|
process.exit(1);
|
|
7040
7763
|
}
|
|
7041
7764
|
});
|
|
@@ -7053,9 +7776,9 @@ projectCmd.command("use <name>").description("Set active project (find or create
|
|
|
7053
7776
|
}
|
|
7054
7777
|
config.activeProject = project.id;
|
|
7055
7778
|
writeFileSync3(CONFIG_PATH2, JSON.stringify(config, null, 2), "utf-8");
|
|
7056
|
-
|
|
7779
|
+
log(chalk5.green(`Active project set to ${chalk5.bold(project.name)} (${project.id})`));
|
|
7057
7780
|
} catch (error) {
|
|
7058
|
-
|
|
7781
|
+
logError(chalk5.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
7059
7782
|
process.exit(1);
|
|
7060
7783
|
}
|
|
7061
7784
|
});
|
|
@@ -7080,40 +7803,44 @@ scheduleCmd.command("create <name>").description("Create a new schedule").requir
|
|
|
7080
7803
|
timeoutMs: opts.timeout ? parseInt(opts.timeout, 10) : undefined,
|
|
7081
7804
|
projectId
|
|
7082
7805
|
});
|
|
7083
|
-
|
|
7806
|
+
log(chalk5.green(`Created schedule ${chalk5.bold(schedule.name)} (${schedule.id})`));
|
|
7084
7807
|
if (schedule.nextRunAt) {
|
|
7085
|
-
|
|
7808
|
+
log(chalk5.dim(` Next run at: ${schedule.nextRunAt}`));
|
|
7086
7809
|
}
|
|
7087
7810
|
} catch (error) {
|
|
7088
|
-
|
|
7811
|
+
logError(chalk5.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
7089
7812
|
process.exit(1);
|
|
7090
7813
|
}
|
|
7091
7814
|
});
|
|
7092
|
-
scheduleCmd.command("list").description("List schedules").option("--project <id>", "Filter by project ID").option("--enabled", "Show only enabled schedules").action((opts) => {
|
|
7815
|
+
scheduleCmd.command("list").description("List schedules").option("--project <id>", "Filter by project ID").option("--enabled", "Show only enabled schedules").option("--json", "Output as JSON", false).action((opts) => {
|
|
7093
7816
|
try {
|
|
7094
7817
|
const projectId = resolveProject(opts.project);
|
|
7095
7818
|
const schedules = listSchedules({
|
|
7096
7819
|
projectId,
|
|
7097
7820
|
enabled: opts.enabled ? true : undefined
|
|
7098
7821
|
});
|
|
7822
|
+
if (opts.json) {
|
|
7823
|
+
log(JSON.stringify(schedules, null, 2));
|
|
7824
|
+
return;
|
|
7825
|
+
}
|
|
7099
7826
|
if (schedules.length === 0) {
|
|
7100
|
-
|
|
7827
|
+
log(chalk5.dim("No schedules found."));
|
|
7101
7828
|
return;
|
|
7102
7829
|
}
|
|
7103
|
-
|
|
7104
|
-
|
|
7105
|
-
|
|
7106
|
-
|
|
7107
|
-
|
|
7830
|
+
log("");
|
|
7831
|
+
log(chalk5.bold(" Schedules"));
|
|
7832
|
+
log("");
|
|
7833
|
+
log(` ${"Name".padEnd(20)} ${"Cron".padEnd(18)} ${"URL".padEnd(30)} ${"Enabled".padEnd(9)} ${"Next Run".padEnd(22)} Last Run`);
|
|
7834
|
+
log(` ${"\u2500".repeat(20)} ${"\u2500".repeat(18)} ${"\u2500".repeat(30)} ${"\u2500".repeat(9)} ${"\u2500".repeat(22)} ${"\u2500".repeat(22)}`);
|
|
7108
7835
|
for (const s of schedules) {
|
|
7109
7836
|
const enabled = s.enabled ? chalk5.green("yes") : chalk5.red("no");
|
|
7110
7837
|
const nextRun = s.nextRunAt ?? chalk5.dim("\u2014");
|
|
7111
7838
|
const lastRun = s.lastRunAt ?? chalk5.dim("\u2014");
|
|
7112
|
-
|
|
7839
|
+
log(` ${s.name.padEnd(20)} ${s.cronExpression.padEnd(18)} ${s.url.padEnd(30)} ${enabled.toString().padEnd(9)} ${nextRun.toString().padEnd(22)} ${lastRun}`);
|
|
7113
7840
|
}
|
|
7114
|
-
|
|
7841
|
+
log("");
|
|
7115
7842
|
} catch (error) {
|
|
7116
|
-
|
|
7843
|
+
logError(chalk5.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
7117
7844
|
process.exit(1);
|
|
7118
7845
|
}
|
|
7119
7846
|
});
|
|
@@ -7121,47 +7848,47 @@ scheduleCmd.command("show <id>").description("Show schedule details").action((id
|
|
|
7121
7848
|
try {
|
|
7122
7849
|
const schedule = getSchedule(id);
|
|
7123
7850
|
if (!schedule) {
|
|
7124
|
-
|
|
7851
|
+
logError(chalk5.red(`Schedule not found: ${id}`));
|
|
7125
7852
|
process.exit(1);
|
|
7126
7853
|
}
|
|
7127
|
-
|
|
7128
|
-
|
|
7129
|
-
|
|
7130
|
-
|
|
7131
|
-
|
|
7132
|
-
|
|
7133
|
-
|
|
7134
|
-
|
|
7135
|
-
|
|
7136
|
-
|
|
7137
|
-
|
|
7138
|
-
|
|
7139
|
-
|
|
7140
|
-
|
|
7141
|
-
|
|
7142
|
-
|
|
7143
|
-
|
|
7144
|
-
|
|
7854
|
+
log("");
|
|
7855
|
+
log(chalk5.bold(` Schedule: ${schedule.name}`));
|
|
7856
|
+
log(` ID: ${schedule.id}`);
|
|
7857
|
+
log(` Cron: ${schedule.cronExpression}`);
|
|
7858
|
+
log(` URL: ${schedule.url}`);
|
|
7859
|
+
log(` Enabled: ${schedule.enabled ? chalk5.green("yes") : chalk5.red("no")}`);
|
|
7860
|
+
log(` Model: ${schedule.model ?? chalk5.dim("default")}`);
|
|
7861
|
+
log(` Headed: ${schedule.headed ? "yes" : "no"}`);
|
|
7862
|
+
log(` Parallel: ${schedule.parallel}`);
|
|
7863
|
+
log(` Timeout: ${schedule.timeoutMs ? `${schedule.timeoutMs}ms` : chalk5.dim("default")}`);
|
|
7864
|
+
log(` Project: ${schedule.projectId ?? chalk5.dim("none")}`);
|
|
7865
|
+
log(` Filter: ${JSON.stringify(schedule.scenarioFilter)}`);
|
|
7866
|
+
log(` Next run: ${schedule.nextRunAt ?? chalk5.dim("not scheduled")}`);
|
|
7867
|
+
log(` Last run: ${schedule.lastRunAt ?? chalk5.dim("never")}`);
|
|
7868
|
+
log(` Last run ID: ${schedule.lastRunId ?? chalk5.dim("none")}`);
|
|
7869
|
+
log(` Created: ${schedule.createdAt}`);
|
|
7870
|
+
log(` Updated: ${schedule.updatedAt}`);
|
|
7871
|
+
log("");
|
|
7145
7872
|
} catch (error) {
|
|
7146
|
-
|
|
7873
|
+
logError(chalk5.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
7147
7874
|
process.exit(1);
|
|
7148
7875
|
}
|
|
7149
7876
|
});
|
|
7150
7877
|
scheduleCmd.command("enable <id>").description("Enable a schedule").action((id) => {
|
|
7151
7878
|
try {
|
|
7152
7879
|
const schedule = updateSchedule(id, { enabled: true });
|
|
7153
|
-
|
|
7880
|
+
log(chalk5.green(`Enabled schedule ${chalk5.bold(schedule.name)}`));
|
|
7154
7881
|
} catch (error) {
|
|
7155
|
-
|
|
7882
|
+
logError(chalk5.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
7156
7883
|
process.exit(1);
|
|
7157
7884
|
}
|
|
7158
7885
|
});
|
|
7159
7886
|
scheduleCmd.command("disable <id>").description("Disable a schedule").action((id) => {
|
|
7160
7887
|
try {
|
|
7161
7888
|
const schedule = updateSchedule(id, { enabled: false });
|
|
7162
|
-
|
|
7889
|
+
log(chalk5.green(`Disabled schedule ${chalk5.bold(schedule.name)}`));
|
|
7163
7890
|
} catch (error) {
|
|
7164
|
-
|
|
7891
|
+
logError(chalk5.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
7165
7892
|
process.exit(1);
|
|
7166
7893
|
}
|
|
7167
7894
|
});
|
|
@@ -7169,13 +7896,13 @@ scheduleCmd.command("delete <id>").description("Delete a schedule").action((id)
|
|
|
7169
7896
|
try {
|
|
7170
7897
|
const deleted = deleteSchedule(id);
|
|
7171
7898
|
if (deleted) {
|
|
7172
|
-
|
|
7899
|
+
log(chalk5.green(`Deleted schedule: ${id}`));
|
|
7173
7900
|
} else {
|
|
7174
|
-
|
|
7901
|
+
logError(chalk5.red(`Schedule not found: ${id}`));
|
|
7175
7902
|
process.exit(1);
|
|
7176
7903
|
}
|
|
7177
7904
|
} catch (error) {
|
|
7178
|
-
|
|
7905
|
+
logError(chalk5.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
7179
7906
|
process.exit(1);
|
|
7180
7907
|
}
|
|
7181
7908
|
});
|
|
@@ -7183,11 +7910,11 @@ scheduleCmd.command("run <id>").description("Manually trigger a schedule").optio
|
|
|
7183
7910
|
try {
|
|
7184
7911
|
const schedule = getSchedule(id);
|
|
7185
7912
|
if (!schedule) {
|
|
7186
|
-
|
|
7913
|
+
logError(chalk5.red(`Schedule not found: ${id}`));
|
|
7187
7914
|
process.exit(1);
|
|
7188
7915
|
return;
|
|
7189
7916
|
}
|
|
7190
|
-
|
|
7917
|
+
log(chalk5.blue(`Running schedule ${chalk5.bold(schedule.name)} against ${schedule.url}...`));
|
|
7191
7918
|
const { run, results } = await runByFilter({
|
|
7192
7919
|
url: schedule.url,
|
|
7193
7920
|
tags: schedule.scenarioFilter.tags,
|
|
@@ -7200,21 +7927,21 @@ scheduleCmd.command("run <id>").description("Manually trigger a schedule").optio
|
|
|
7200
7927
|
projectId: schedule.projectId ?? undefined
|
|
7201
7928
|
});
|
|
7202
7929
|
if (opts.json) {
|
|
7203
|
-
|
|
7930
|
+
log(formatJSON(run, results));
|
|
7204
7931
|
} else {
|
|
7205
|
-
|
|
7932
|
+
log(formatTerminal(run, results));
|
|
7206
7933
|
}
|
|
7207
7934
|
process.exit(getExitCode(run));
|
|
7208
7935
|
} catch (error) {
|
|
7209
|
-
|
|
7936
|
+
logError(chalk5.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
7210
7937
|
process.exit(1);
|
|
7211
7938
|
}
|
|
7212
7939
|
});
|
|
7213
7940
|
program2.command("daemon").description("Start the scheduler daemon").option("--interval <seconds>", "Check interval in seconds", "60").action(async (opts) => {
|
|
7214
7941
|
try {
|
|
7215
7942
|
const intervalMs = parseInt(opts.interval, 10) * 1000;
|
|
7216
|
-
|
|
7217
|
-
|
|
7943
|
+
log(chalk5.blue("Scheduler daemon started. Press Ctrl+C to stop."));
|
|
7944
|
+
log(chalk5.dim(` Check interval: ${opts.interval}s`));
|
|
7218
7945
|
let running = true;
|
|
7219
7946
|
const checkAndRun = async () => {
|
|
7220
7947
|
while (running) {
|
|
@@ -7223,7 +7950,7 @@ program2.command("daemon").description("Start the scheduler daemon").option("--i
|
|
|
7223
7950
|
const now2 = new Date().toISOString();
|
|
7224
7951
|
for (const schedule of schedules) {
|
|
7225
7952
|
if (schedule.nextRunAt && schedule.nextRunAt <= now2) {
|
|
7226
|
-
|
|
7953
|
+
log(chalk5.blue(`[${new Date().toISOString()}] Triggering schedule: ${schedule.name}`));
|
|
7227
7954
|
try {
|
|
7228
7955
|
const { run } = await runByFilter({
|
|
7229
7956
|
url: schedule.url,
|
|
@@ -7237,60 +7964,60 @@ program2.command("daemon").description("Start the scheduler daemon").option("--i
|
|
|
7237
7964
|
projectId: schedule.projectId ?? undefined
|
|
7238
7965
|
});
|
|
7239
7966
|
const statusColor = run.status === "passed" ? chalk5.green : chalk5.red;
|
|
7240
|
-
|
|
7967
|
+
log(` ${statusColor(run.status)} \u2014 ${run.passed}/${run.total} passed`);
|
|
7241
7968
|
updateSchedule(schedule.id, {});
|
|
7242
7969
|
} catch (err) {
|
|
7243
|
-
|
|
7970
|
+
logError(chalk5.red(` Error running schedule ${schedule.name}: ${err instanceof Error ? err.message : String(err)}`));
|
|
7244
7971
|
}
|
|
7245
7972
|
}
|
|
7246
7973
|
}
|
|
7247
7974
|
} catch (err) {
|
|
7248
|
-
|
|
7975
|
+
logError(chalk5.red(`Daemon error: ${err instanceof Error ? err.message : String(err)}`));
|
|
7249
7976
|
}
|
|
7250
7977
|
await new Promise((resolve2) => setTimeout(resolve2, intervalMs));
|
|
7251
7978
|
}
|
|
7252
7979
|
};
|
|
7253
7980
|
process.on("SIGINT", () => {
|
|
7254
|
-
|
|
7981
|
+
log(chalk5.yellow(`
|
|
7255
7982
|
Shutting down scheduler daemon...`));
|
|
7256
7983
|
running = false;
|
|
7257
7984
|
process.exit(0);
|
|
7258
7985
|
});
|
|
7259
7986
|
process.on("SIGTERM", () => {
|
|
7260
|
-
|
|
7987
|
+
log(chalk5.yellow(`
|
|
7261
7988
|
Shutting down scheduler daemon...`));
|
|
7262
7989
|
running = false;
|
|
7263
7990
|
process.exit(0);
|
|
7264
7991
|
});
|
|
7265
7992
|
await checkAndRun();
|
|
7266
7993
|
} catch (error) {
|
|
7267
|
-
|
|
7994
|
+
logError(chalk5.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
7268
7995
|
process.exit(1);
|
|
7269
7996
|
}
|
|
7270
7997
|
});
|
|
7271
|
-
program2.command("init").description("Initialize a new testing project").option("-n, --name <name>", "Project name").option("-u, --url <url>", "Base URL").option("-p, --path <path>", "Project path").option("--ci <provider>", "Generate CI workflow (github)").action((opts) => {
|
|
7998
|
+
program2.command("init").description("Initialize a new testing project").option("-n, --name <name>", "Project name").option("-u, --url <url>", "Base URL").option("-p, --path <path>", "Project path").option("--ci <provider>", "Generate CI workflow (github)").action(async (opts) => {
|
|
7272
7999
|
try {
|
|
7273
|
-
const { project, scenarios, framework } = initProject({
|
|
8000
|
+
const { project, scenarios, framework, url } = initProject({
|
|
7274
8001
|
name: opts.name,
|
|
7275
8002
|
url: opts.url,
|
|
7276
8003
|
path: opts.path
|
|
7277
8004
|
});
|
|
7278
|
-
|
|
7279
|
-
|
|
7280
|
-
|
|
8005
|
+
log("");
|
|
8006
|
+
log(chalk5.bold(" Project initialized!"));
|
|
8007
|
+
log("");
|
|
7281
8008
|
if (framework) {
|
|
7282
|
-
|
|
8009
|
+
log(` Framework: ${chalk5.cyan(framework.name)}`);
|
|
7283
8010
|
if (framework.features.length > 0) {
|
|
7284
|
-
|
|
8011
|
+
log(` Features: ${chalk5.dim(framework.features.join(", "))}`);
|
|
7285
8012
|
}
|
|
7286
8013
|
} else {
|
|
7287
|
-
|
|
8014
|
+
log(` Framework: ${chalk5.dim("not detected")}`);
|
|
7288
8015
|
}
|
|
7289
|
-
|
|
7290
|
-
|
|
7291
|
-
|
|
8016
|
+
log(` Project: ${chalk5.green(project.name)} ${chalk5.dim(`(${project.id})`)}`);
|
|
8017
|
+
log(` Scenarios: ${chalk5.green(String(scenarios.length))} starter scenarios created`);
|
|
8018
|
+
log("");
|
|
7292
8019
|
for (const s of scenarios) {
|
|
7293
|
-
|
|
8020
|
+
log(` ${chalk5.dim(s.shortId)} ${s.name} ${chalk5.dim(`[${s.tags.join(", ")}]`)}`);
|
|
7294
8021
|
}
|
|
7295
8022
|
if (opts.ci === "github") {
|
|
7296
8023
|
const workflowDir = join6(process.cwd(), ".github", "workflows");
|
|
@@ -7299,18 +8026,51 @@ program2.command("init").description("Initialize a new testing project").option(
|
|
|
7299
8026
|
}
|
|
7300
8027
|
const workflowPath = join6(workflowDir, "testers.yml");
|
|
7301
8028
|
writeFileSync3(workflowPath, generateGitHubActionsWorkflow(), "utf-8");
|
|
7302
|
-
|
|
8029
|
+
log(` CI: ${chalk5.green("GitHub Actions workflow written to .github/workflows/testers.yml")}`);
|
|
7303
8030
|
} else if (opts.ci) {
|
|
7304
|
-
|
|
7305
|
-
}
|
|
7306
|
-
|
|
7307
|
-
|
|
7308
|
-
|
|
7309
|
-
|
|
7310
|
-
|
|
7311
|
-
|
|
8031
|
+
log(chalk5.yellow(` Unknown CI provider: ${opts.ci}. Supported: github`));
|
|
8032
|
+
}
|
|
8033
|
+
log("");
|
|
8034
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
8035
|
+
const ask = (q) => new Promise((resolve2) => rl.question(q, resolve2));
|
|
8036
|
+
try {
|
|
8037
|
+
const envAnswer = await ask(" Would you like to configure environments? [y/N] ");
|
|
8038
|
+
if (envAnswer.trim().toLowerCase() === "y") {
|
|
8039
|
+
const envName = await ask(" Environment name (default: staging): ");
|
|
8040
|
+
const envUrl = await ask(` Base URL (default: ${url}): `);
|
|
8041
|
+
const resolvedEnvName = envName.trim() || "staging";
|
|
8042
|
+
const resolvedEnvUrl = envUrl.trim() || url;
|
|
8043
|
+
createEnvironment({ name: resolvedEnvName, url: resolvedEnvUrl, projectId: project.id, isDefault: true });
|
|
8044
|
+
log(chalk5.green(` \u2713 Environment '${resolvedEnvName}' created (${resolvedEnvUrl})`));
|
|
8045
|
+
log("");
|
|
8046
|
+
}
|
|
8047
|
+
const scenarioAnswer = await ask(" Would you like to create your first test scenario? [y/N] ");
|
|
8048
|
+
if (scenarioAnswer.trim().toLowerCase() === "y") {
|
|
8049
|
+
const scenarioName = await ask(" Scenario name: ");
|
|
8050
|
+
const scenarioUrl = await ask(` URL to test (default: ${url}): `);
|
|
8051
|
+
const resolvedScenarioName = scenarioName.trim() || "My first scenario";
|
|
8052
|
+
const resolvedScenarioUrl = scenarioUrl.trim() || url;
|
|
8053
|
+
const newScenario = createScenario({
|
|
8054
|
+
name: resolvedScenarioName,
|
|
8055
|
+
description: `Navigate to ${resolvedScenarioUrl} and verify it loads correctly.`,
|
|
8056
|
+
projectId: project.id,
|
|
8057
|
+
targetPath: resolvedScenarioUrl,
|
|
8058
|
+
tags: ["smoke"],
|
|
8059
|
+
priority: "high"
|
|
8060
|
+
});
|
|
8061
|
+
log(chalk5.green(` \u2713 Scenario '${newScenario.name}' created ${chalk5.dim(`(${newScenario.shortId})`)}`));
|
|
8062
|
+
log("");
|
|
8063
|
+
}
|
|
8064
|
+
} finally {
|
|
8065
|
+
rl.close();
|
|
8066
|
+
}
|
|
8067
|
+
log(chalk5.bold(" Next steps:"));
|
|
8068
|
+
log(` 1. Start your dev server`);
|
|
8069
|
+
log(` 2. Run ${chalk5.cyan("testers run <url>")} to execute tests`);
|
|
8070
|
+
log(` 3. Add more scenarios with ${chalk5.cyan("testers add <name>")}`);
|
|
8071
|
+
log("");
|
|
7312
8072
|
} catch (error) {
|
|
7313
|
-
|
|
8073
|
+
logError(chalk5.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
7314
8074
|
process.exit(1);
|
|
7315
8075
|
}
|
|
7316
8076
|
});
|
|
@@ -7318,16 +8078,16 @@ program2.command("replay <run-id>").description("Re-run all scenarios from a pre
|
|
|
7318
8078
|
try {
|
|
7319
8079
|
const originalRun = getRun(runId);
|
|
7320
8080
|
if (!originalRun) {
|
|
7321
|
-
|
|
8081
|
+
logError(chalk5.red(`Run not found: ${runId}`));
|
|
7322
8082
|
process.exit(1);
|
|
7323
8083
|
}
|
|
7324
8084
|
const originalResults = getResultsByRun(originalRun.id);
|
|
7325
8085
|
const scenarioIds = originalResults.map((r) => r.scenarioId);
|
|
7326
8086
|
if (scenarioIds.length === 0) {
|
|
7327
|
-
|
|
8087
|
+
log(chalk5.dim("No scenarios to replay."));
|
|
7328
8088
|
return;
|
|
7329
8089
|
}
|
|
7330
|
-
|
|
8090
|
+
log(chalk5.blue(`Replaying ${scenarioIds.length} scenarios from run ${originalRun.id.slice(0, 8)}...`));
|
|
7331
8091
|
const { run, results } = await runByFilter({
|
|
7332
8092
|
url: opts.url ?? originalRun.url,
|
|
7333
8093
|
scenarioIds,
|
|
@@ -7336,13 +8096,13 @@ program2.command("replay <run-id>").description("Re-run all scenarios from a pre
|
|
|
7336
8096
|
parallel: parseInt(opts.parallel, 10)
|
|
7337
8097
|
});
|
|
7338
8098
|
if (opts.json) {
|
|
7339
|
-
|
|
8099
|
+
log(formatJSON(run, results));
|
|
7340
8100
|
} else {
|
|
7341
|
-
|
|
8101
|
+
log(formatTerminal(run, results));
|
|
7342
8102
|
}
|
|
7343
8103
|
process.exit(getExitCode(run));
|
|
7344
8104
|
} catch (error) {
|
|
7345
|
-
|
|
8105
|
+
logError(chalk5.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
7346
8106
|
process.exit(1);
|
|
7347
8107
|
}
|
|
7348
8108
|
});
|
|
@@ -7350,16 +8110,16 @@ program2.command("retry <run-id>").description("Re-run only failed scenarios fro
|
|
|
7350
8110
|
try {
|
|
7351
8111
|
const originalRun = getRun(runId);
|
|
7352
8112
|
if (!originalRun) {
|
|
7353
|
-
|
|
8113
|
+
logError(chalk5.red(`Run not found: ${runId}`));
|
|
7354
8114
|
process.exit(1);
|
|
7355
8115
|
}
|
|
7356
8116
|
const originalResults = getResultsByRun(originalRun.id);
|
|
7357
8117
|
const failedScenarioIds = originalResults.filter((r) => r.status === "failed" || r.status === "error").map((r) => r.scenarioId);
|
|
7358
8118
|
if (failedScenarioIds.length === 0) {
|
|
7359
|
-
|
|
8119
|
+
log(chalk5.green("No failed scenarios to retry. All passed!"));
|
|
7360
8120
|
return;
|
|
7361
8121
|
}
|
|
7362
|
-
|
|
8122
|
+
log(chalk5.blue(`Retrying ${failedScenarioIds.length} failed scenarios from run ${originalRun.id.slice(0, 8)}...`));
|
|
7363
8123
|
const { run, results } = await runByFilter({
|
|
7364
8124
|
url: opts.url ?? originalRun.url,
|
|
7365
8125
|
scenarioIds: failedScenarioIds,
|
|
@@ -7368,35 +8128,35 @@ program2.command("retry <run-id>").description("Re-run only failed scenarios fro
|
|
|
7368
8128
|
parallel: parseInt(opts.parallel, 10)
|
|
7369
8129
|
});
|
|
7370
8130
|
if (!opts.json) {
|
|
7371
|
-
|
|
7372
|
-
|
|
8131
|
+
log("");
|
|
8132
|
+
log(chalk5.bold(" Comparison with original run:"));
|
|
7373
8133
|
for (const result of results) {
|
|
7374
8134
|
const original = originalResults.find((r) => r.scenarioId === result.scenarioId);
|
|
7375
8135
|
if (original) {
|
|
7376
8136
|
const changed = original.status !== result.status;
|
|
7377
8137
|
const arrow = changed ? chalk5.yellow(`${original.status} \u2192 ${result.status}`) : chalk5.dim(`${result.status} (unchanged)`);
|
|
7378
8138
|
const icon = result.status === "passed" ? chalk5.green("\u2713") : chalk5.red("\u2717");
|
|
7379
|
-
|
|
8139
|
+
log(` ${icon} ${result.scenarioId.slice(0, 8)}: ${arrow}`);
|
|
7380
8140
|
}
|
|
7381
8141
|
}
|
|
7382
|
-
|
|
8142
|
+
log("");
|
|
7383
8143
|
}
|
|
7384
8144
|
if (opts.json) {
|
|
7385
|
-
|
|
8145
|
+
log(formatJSON(run, results));
|
|
7386
8146
|
} else {
|
|
7387
|
-
|
|
8147
|
+
log(formatTerminal(run, results));
|
|
7388
8148
|
}
|
|
7389
8149
|
process.exit(getExitCode(run));
|
|
7390
8150
|
} catch (error) {
|
|
7391
|
-
|
|
8151
|
+
logError(chalk5.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
7392
8152
|
process.exit(1);
|
|
7393
8153
|
}
|
|
7394
8154
|
});
|
|
7395
8155
|
program2.command("smoke <url>").description("Run autonomous smoke test").option("-m, --model <model>", "AI model").option("--headed", "Watch browser", false).option("--timeout <ms>", "Timeout in milliseconds").option("--json", "JSON output", false).option("--project <id>", "Project ID").action(async (url, opts) => {
|
|
7396
8156
|
try {
|
|
7397
8157
|
const projectId = resolveProject(opts.project);
|
|
7398
|
-
|
|
7399
|
-
|
|
8158
|
+
log(chalk5.blue(`Running smoke test against ${chalk5.bold(url)}...`));
|
|
8159
|
+
log("");
|
|
7400
8160
|
const smokeResult = await runSmoke({
|
|
7401
8161
|
url,
|
|
7402
8162
|
model: opts.model,
|
|
@@ -7405,19 +8165,19 @@ program2.command("smoke <url>").description("Run autonomous smoke test").option(
|
|
|
7405
8165
|
projectId
|
|
7406
8166
|
});
|
|
7407
8167
|
if (opts.json) {
|
|
7408
|
-
|
|
8168
|
+
log(JSON.stringify({
|
|
7409
8169
|
run: smokeResult.run,
|
|
7410
8170
|
result: smokeResult.result,
|
|
7411
8171
|
pagesVisited: smokeResult.pagesVisited,
|
|
7412
8172
|
issues: smokeResult.issuesFound
|
|
7413
8173
|
}, null, 2));
|
|
7414
8174
|
} else {
|
|
7415
|
-
|
|
8175
|
+
log(formatSmokeReport(smokeResult));
|
|
7416
8176
|
}
|
|
7417
8177
|
const hasCritical = smokeResult.issuesFound.some((i) => i.severity === "critical" || i.severity === "high");
|
|
7418
8178
|
process.exit(hasCritical ? 1 : 0);
|
|
7419
8179
|
} catch (error) {
|
|
7420
|
-
|
|
8180
|
+
logError(chalk5.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
7421
8181
|
process.exit(1);
|
|
7422
8182
|
}
|
|
7423
8183
|
});
|
|
@@ -7425,27 +8185,27 @@ program2.command("diff <run1> <run2>").description("Compare two test runs").opti
|
|
|
7425
8185
|
try {
|
|
7426
8186
|
const diff = diffRuns(run1, run2);
|
|
7427
8187
|
if (opts.json) {
|
|
7428
|
-
|
|
8188
|
+
log(formatDiffJSON(diff));
|
|
7429
8189
|
} else {
|
|
7430
|
-
|
|
8190
|
+
log(formatDiffTerminal(diff));
|
|
7431
8191
|
}
|
|
7432
8192
|
const threshold = parseFloat(opts.threshold);
|
|
7433
8193
|
const visualResults = compareRunScreenshots(run2, run1, threshold);
|
|
7434
8194
|
if (visualResults.length > 0) {
|
|
7435
8195
|
if (opts.json) {
|
|
7436
|
-
|
|
8196
|
+
log(JSON.stringify({ visualDiff: visualResults }, null, 2));
|
|
7437
8197
|
} else {
|
|
7438
|
-
|
|
8198
|
+
log(formatVisualDiffTerminal(visualResults, threshold));
|
|
7439
8199
|
}
|
|
7440
8200
|
}
|
|
7441
8201
|
const hasVisualRegressions = visualResults.some((r) => r.isRegression);
|
|
7442
8202
|
process.exit(diff.regressions.length > 0 || hasVisualRegressions ? 1 : 0);
|
|
7443
8203
|
} catch (error) {
|
|
7444
|
-
|
|
8204
|
+
logError(chalk5.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
7445
8205
|
process.exit(1);
|
|
7446
8206
|
}
|
|
7447
8207
|
});
|
|
7448
|
-
program2.command("report [run-id]").description("Generate HTML test report").option("--latest", "Use most recent run", false).option("-o, --output <file>", "Output file path", "report.html").action((runId, opts) => {
|
|
8208
|
+
program2.command("report [run-id]").description("Generate HTML test report").option("--latest", "Use most recent run", false).option("-o, --output <file>", "Output file path", "report.html").option("--open", "Open the report in the browser after generating", false).action((runId, opts) => {
|
|
7449
8209
|
try {
|
|
7450
8210
|
let html;
|
|
7451
8211
|
if (opts.latest || !runId) {
|
|
@@ -7454,9 +8214,14 @@ program2.command("report [run-id]").description("Generate HTML test report").opt
|
|
|
7454
8214
|
html = generateHtmlReport(runId);
|
|
7455
8215
|
}
|
|
7456
8216
|
writeFileSync3(opts.output, html, "utf-8");
|
|
7457
|
-
|
|
8217
|
+
const absPath = resolve(opts.output);
|
|
8218
|
+
log(chalk5.green(`Report generated: ${absPath}`));
|
|
8219
|
+
if (opts.open) {
|
|
8220
|
+
const openCmd = process.platform === "darwin" ? "open" : "xdg-open";
|
|
8221
|
+
Bun.spawn([openCmd, absPath]);
|
|
8222
|
+
}
|
|
7458
8223
|
} catch (error) {
|
|
7459
|
-
|
|
8224
|
+
logError(chalk5.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
7460
8225
|
process.exit(1);
|
|
7461
8226
|
}
|
|
7462
8227
|
});
|
|
@@ -7469,9 +8234,9 @@ authCmd.command("add <name>").description("Create an auth preset").requiredOptio
|
|
|
7469
8234
|
password: opts.password,
|
|
7470
8235
|
loginPath: opts.loginPath
|
|
7471
8236
|
});
|
|
7472
|
-
|
|
8237
|
+
log(chalk5.green(`Created auth preset ${chalk5.bold(preset.name)} (${preset.email})`));
|
|
7473
8238
|
} catch (error) {
|
|
7474
|
-
|
|
8239
|
+
logError(chalk5.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
7475
8240
|
process.exit(1);
|
|
7476
8241
|
}
|
|
7477
8242
|
});
|
|
@@ -7479,20 +8244,20 @@ authCmd.command("list").description("List auth presets").action(() => {
|
|
|
7479
8244
|
try {
|
|
7480
8245
|
const presets = listAuthPresets();
|
|
7481
8246
|
if (presets.length === 0) {
|
|
7482
|
-
|
|
8247
|
+
log(chalk5.dim("No auth presets found."));
|
|
7483
8248
|
return;
|
|
7484
8249
|
}
|
|
7485
|
-
|
|
7486
|
-
|
|
7487
|
-
|
|
7488
|
-
|
|
7489
|
-
|
|
8250
|
+
log("");
|
|
8251
|
+
log(chalk5.bold(" Auth Presets"));
|
|
8252
|
+
log("");
|
|
8253
|
+
log(` ${"Name".padEnd(20)} ${"Email".padEnd(30)} ${"Login Path".padEnd(15)} Created`);
|
|
8254
|
+
log(` ${"\u2500".repeat(20)} ${"\u2500".repeat(30)} ${"\u2500".repeat(15)} ${"\u2500".repeat(22)}`);
|
|
7490
8255
|
for (const p of presets) {
|
|
7491
|
-
|
|
8256
|
+
log(` ${p.name.padEnd(20)} ${p.email.padEnd(30)} ${p.loginPath.padEnd(15)} ${p.createdAt}`);
|
|
7492
8257
|
}
|
|
7493
|
-
|
|
8258
|
+
log("");
|
|
7494
8259
|
} catch (error) {
|
|
7495
|
-
|
|
8260
|
+
logError(chalk5.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
7496
8261
|
process.exit(1);
|
|
7497
8262
|
}
|
|
7498
8263
|
});
|
|
@@ -7500,26 +8265,28 @@ authCmd.command("delete <name>").description("Delete an auth preset").action((na
|
|
|
7500
8265
|
try {
|
|
7501
8266
|
const deleted = deleteAuthPreset(name);
|
|
7502
8267
|
if (deleted) {
|
|
7503
|
-
|
|
8268
|
+
log(chalk5.green(`Deleted auth preset: ${name}`));
|
|
7504
8269
|
} else {
|
|
7505
|
-
|
|
8270
|
+
logError(chalk5.red(`Auth preset not found: ${name}`));
|
|
7506
8271
|
process.exit(1);
|
|
7507
8272
|
}
|
|
7508
8273
|
} catch (error) {
|
|
7509
|
-
|
|
8274
|
+
logError(chalk5.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
7510
8275
|
process.exit(1);
|
|
7511
8276
|
}
|
|
7512
8277
|
});
|
|
7513
|
-
program2.command("costs").description("Show cost tracking and budget status").option("--project <id>", "Project ID").option("--period <period>", "Time period", "month").option("--json", "JSON output", false).action((opts) => {
|
|
8278
|
+
program2.command("costs").description("Show cost tracking and budget status").option("--project <id>", "Project ID").option("--period <period>", "Time period", "month").option("--json", "JSON output", false).option("--csv", "CSV output", false).action((opts) => {
|
|
7514
8279
|
try {
|
|
7515
8280
|
const summary = getCostSummary({ projectId: resolveProject(opts.project), period: opts.period });
|
|
7516
|
-
if (opts.
|
|
7517
|
-
|
|
8281
|
+
if (opts.csv) {
|
|
8282
|
+
log(formatCostsCsv(summary));
|
|
8283
|
+
} else if (opts.json) {
|
|
8284
|
+
log(formatCostsJSON(summary));
|
|
7518
8285
|
} else {
|
|
7519
|
-
|
|
8286
|
+
log(formatCostsTerminal(summary));
|
|
7520
8287
|
}
|
|
7521
8288
|
} catch (error) {
|
|
7522
|
-
|
|
8289
|
+
logError(chalk5.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
7523
8290
|
process.exit(1);
|
|
7524
8291
|
}
|
|
7525
8292
|
});
|
|
@@ -7527,18 +8294,18 @@ program2.command("chain <scenario-id>").description("Add a dependency to a scena
|
|
|
7527
8294
|
try {
|
|
7528
8295
|
const scenario = getScenario(scenarioId) ?? getScenarioByShortId(scenarioId);
|
|
7529
8296
|
if (!scenario) {
|
|
7530
|
-
|
|
8297
|
+
logError(chalk5.red(`Scenario not found: ${scenarioId}`));
|
|
7531
8298
|
process.exit(1);
|
|
7532
8299
|
}
|
|
7533
8300
|
const dep = getScenario(opts.dependsOn) ?? getScenarioByShortId(opts.dependsOn);
|
|
7534
8301
|
if (!dep) {
|
|
7535
|
-
|
|
8302
|
+
logError(chalk5.red(`Dependency scenario not found: ${opts.dependsOn}`));
|
|
7536
8303
|
process.exit(1);
|
|
7537
8304
|
}
|
|
7538
8305
|
addDependency(scenario.id, dep.id);
|
|
7539
|
-
|
|
8306
|
+
log(chalk5.green(`${scenario.shortId} now depends on ${dep.shortId}`));
|
|
7540
8307
|
} catch (error) {
|
|
7541
|
-
|
|
8308
|
+
logError(chalk5.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
7542
8309
|
process.exit(1);
|
|
7543
8310
|
}
|
|
7544
8311
|
});
|
|
@@ -7546,18 +8313,18 @@ program2.command("unchain <scenario-id>").description("Remove a dependency from
|
|
|
7546
8313
|
try {
|
|
7547
8314
|
const scenario = getScenario(scenarioId) ?? getScenarioByShortId(scenarioId);
|
|
7548
8315
|
if (!scenario) {
|
|
7549
|
-
|
|
8316
|
+
logError(chalk5.red(`Scenario not found: ${scenarioId}`));
|
|
7550
8317
|
process.exit(1);
|
|
7551
8318
|
}
|
|
7552
8319
|
const dep = getScenario(opts.from) ?? getScenarioByShortId(opts.from);
|
|
7553
8320
|
if (!dep) {
|
|
7554
|
-
|
|
8321
|
+
logError(chalk5.red(`Dependency not found: ${opts.from}`));
|
|
7555
8322
|
process.exit(1);
|
|
7556
8323
|
}
|
|
7557
8324
|
removeDependency(scenario.id, dep.id);
|
|
7558
|
-
|
|
8325
|
+
log(chalk5.green(`Removed dependency: ${scenario.shortId} no longer depends on ${dep.shortId}`));
|
|
7559
8326
|
} catch (error) {
|
|
7560
|
-
|
|
8327
|
+
logError(chalk5.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
7561
8328
|
process.exit(1);
|
|
7562
8329
|
}
|
|
7563
8330
|
});
|
|
@@ -7565,34 +8332,34 @@ program2.command("deps <scenario-id>").description("Show dependencies for a scen
|
|
|
7565
8332
|
try {
|
|
7566
8333
|
const scenario = getScenario(scenarioId) ?? getScenarioByShortId(scenarioId);
|
|
7567
8334
|
if (!scenario) {
|
|
7568
|
-
|
|
8335
|
+
logError(chalk5.red(`Scenario not found: ${scenarioId}`));
|
|
7569
8336
|
process.exit(1);
|
|
7570
8337
|
}
|
|
7571
8338
|
const deps = getDependencies(scenario.id);
|
|
7572
8339
|
const dependents = getDependents(scenario.id);
|
|
7573
|
-
|
|
7574
|
-
|
|
7575
|
-
|
|
8340
|
+
log("");
|
|
8341
|
+
log(chalk5.bold(` Dependencies for ${scenario.shortId}: ${scenario.name}`));
|
|
8342
|
+
log("");
|
|
7576
8343
|
if (deps.length > 0) {
|
|
7577
|
-
|
|
8344
|
+
log(chalk5.dim(" Depends on:"));
|
|
7578
8345
|
for (const depId of deps) {
|
|
7579
8346
|
const s = getScenario(depId);
|
|
7580
|
-
|
|
8347
|
+
log(` \u2192 ${s ? `${s.shortId}: ${s.name}` : depId.slice(0, 8)}`);
|
|
7581
8348
|
}
|
|
7582
8349
|
} else {
|
|
7583
|
-
|
|
8350
|
+
log(chalk5.dim(" No dependencies"));
|
|
7584
8351
|
}
|
|
7585
8352
|
if (dependents.length > 0) {
|
|
7586
|
-
|
|
7587
|
-
|
|
8353
|
+
log("");
|
|
8354
|
+
log(chalk5.dim(" Required by:"));
|
|
7588
8355
|
for (const depId of dependents) {
|
|
7589
8356
|
const s = getScenario(depId);
|
|
7590
|
-
|
|
8357
|
+
log(` \u2190 ${s ? `${s.shortId}: ${s.name}` : depId.slice(0, 8)}`);
|
|
7591
8358
|
}
|
|
7592
8359
|
}
|
|
7593
|
-
|
|
8360
|
+
log("");
|
|
7594
8361
|
} catch (error) {
|
|
7595
|
-
|
|
8362
|
+
logError(chalk5.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
7596
8363
|
process.exit(1);
|
|
7597
8364
|
}
|
|
7598
8365
|
});
|
|
@@ -7602,7 +8369,7 @@ flowCmd.command("create <name>").description("Create a flow from scenario IDs").
|
|
|
7602
8369
|
const ids = opts.chain.split(",").map((id) => {
|
|
7603
8370
|
const s = getScenario(id.trim()) ?? getScenarioByShortId(id.trim());
|
|
7604
8371
|
if (!s) {
|
|
7605
|
-
|
|
8372
|
+
logError(chalk5.red(`Scenario not found: ${id.trim()}`));
|
|
7606
8373
|
process.exit(1);
|
|
7607
8374
|
}
|
|
7608
8375
|
return s.id;
|
|
@@ -7613,49 +8380,49 @@ flowCmd.command("create <name>").description("Create a flow from scenario IDs").
|
|
|
7613
8380
|
} catch {}
|
|
7614
8381
|
}
|
|
7615
8382
|
const flow = createFlow({ name, scenarioIds: ids, projectId: resolveProject(opts.project) });
|
|
7616
|
-
|
|
8383
|
+
log(chalk5.green(`Flow created: ${flow.id.slice(0, 8)} \u2014 ${flow.name} (${ids.length} scenarios)`));
|
|
7617
8384
|
} catch (error) {
|
|
7618
|
-
|
|
8385
|
+
logError(chalk5.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
7619
8386
|
process.exit(1);
|
|
7620
8387
|
}
|
|
7621
8388
|
});
|
|
7622
8389
|
flowCmd.command("list").description("List all flows").option("--project <id>", "Project ID").action((opts) => {
|
|
7623
8390
|
const flows = listFlows(resolveProject(opts.project) ?? undefined);
|
|
7624
8391
|
if (flows.length === 0) {
|
|
7625
|
-
|
|
8392
|
+
log(chalk5.dim(`
|
|
7626
8393
|
No flows found.
|
|
7627
8394
|
`));
|
|
7628
8395
|
return;
|
|
7629
8396
|
}
|
|
7630
|
-
|
|
7631
|
-
|
|
7632
|
-
|
|
8397
|
+
log("");
|
|
8398
|
+
log(chalk5.bold(" Flows"));
|
|
8399
|
+
log("");
|
|
7633
8400
|
for (const f of flows) {
|
|
7634
|
-
|
|
8401
|
+
log(` ${chalk5.dim(f.id.slice(0, 8))} ${f.name} ${chalk5.dim(`(${f.scenarioIds.length} scenarios)`)}`);
|
|
7635
8402
|
}
|
|
7636
|
-
|
|
8403
|
+
log("");
|
|
7637
8404
|
});
|
|
7638
8405
|
flowCmd.command("show <id>").description("Show flow details").action((id) => {
|
|
7639
8406
|
const flow = getFlow(id);
|
|
7640
8407
|
if (!flow) {
|
|
7641
|
-
|
|
8408
|
+
logError(chalk5.red(`Flow not found: ${id}`));
|
|
7642
8409
|
process.exit(1);
|
|
7643
8410
|
}
|
|
7644
|
-
|
|
7645
|
-
|
|
7646
|
-
|
|
7647
|
-
|
|
8411
|
+
log("");
|
|
8412
|
+
log(chalk5.bold(` Flow: ${flow.name}`));
|
|
8413
|
+
log(` ID: ${chalk5.dim(flow.id)}`);
|
|
8414
|
+
log(` Scenarios (in order):`);
|
|
7648
8415
|
for (let i = 0;i < flow.scenarioIds.length; i++) {
|
|
7649
8416
|
const s = getScenario(flow.scenarioIds[i]);
|
|
7650
|
-
|
|
8417
|
+
log(` ${i + 1}. ${s ? `${s.shortId}: ${s.name}` : flow.scenarioIds[i].slice(0, 8)}`);
|
|
7651
8418
|
}
|
|
7652
|
-
|
|
8419
|
+
log("");
|
|
7653
8420
|
});
|
|
7654
8421
|
flowCmd.command("delete <id>").description("Delete a flow").action((id) => {
|
|
7655
8422
|
if (deleteFlow(id))
|
|
7656
|
-
|
|
8423
|
+
log(chalk5.green("Flow deleted."));
|
|
7657
8424
|
else {
|
|
7658
|
-
|
|
8425
|
+
logError(chalk5.red("Flow not found."));
|
|
7659
8426
|
process.exit(1);
|
|
7660
8427
|
}
|
|
7661
8428
|
});
|
|
@@ -7663,14 +8430,14 @@ flowCmd.command("run <id>").description("Run a flow (scenarios in dependency ord
|
|
|
7663
8430
|
try {
|
|
7664
8431
|
const flow = getFlow(id);
|
|
7665
8432
|
if (!flow) {
|
|
7666
|
-
|
|
8433
|
+
logError(chalk5.red(`Flow not found: ${id}`));
|
|
7667
8434
|
process.exit(1);
|
|
7668
8435
|
}
|
|
7669
8436
|
if (!opts.url) {
|
|
7670
|
-
|
|
8437
|
+
logError(chalk5.red("--url is required for flow run"));
|
|
7671
8438
|
process.exit(1);
|
|
7672
8439
|
}
|
|
7673
|
-
|
|
8440
|
+
log(chalk5.blue(`Running flow: ${flow.name} (${flow.scenarioIds.length} scenarios)`));
|
|
7674
8441
|
const { run, results } = await runByFilter({
|
|
7675
8442
|
url: opts.url,
|
|
7676
8443
|
scenarioIds: flow.scenarioIds,
|
|
@@ -7679,12 +8446,12 @@ flowCmd.command("run <id>").description("Run a flow (scenarios in dependency ord
|
|
|
7679
8446
|
parallel: 1
|
|
7680
8447
|
});
|
|
7681
8448
|
if (opts.json)
|
|
7682
|
-
|
|
8449
|
+
log(formatJSON(run, results));
|
|
7683
8450
|
else
|
|
7684
|
-
|
|
8451
|
+
log(formatTerminal(run, results));
|
|
7685
8452
|
process.exit(getExitCode(run));
|
|
7686
8453
|
} catch (error) {
|
|
7687
|
-
|
|
8454
|
+
logError(chalk5.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
7688
8455
|
process.exit(1);
|
|
7689
8456
|
}
|
|
7690
8457
|
});
|
|
@@ -7698,9 +8465,9 @@ envCmd.command("add <name>").description("Add a named environment").requiredOpti
|
|
|
7698
8465
|
projectId: opts.project,
|
|
7699
8466
|
isDefault: opts.default
|
|
7700
8467
|
});
|
|
7701
|
-
|
|
8468
|
+
log(chalk5.green(`Environment added: ${env.name} \u2192 ${env.url}${env.isDefault ? " (default)" : ""}`));
|
|
7702
8469
|
} catch (error) {
|
|
7703
|
-
|
|
8470
|
+
logError(chalk5.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
7704
8471
|
process.exit(1);
|
|
7705
8472
|
}
|
|
7706
8473
|
});
|
|
@@ -7708,16 +8475,16 @@ envCmd.command("list").description("List all environments").option("--project <i
|
|
|
7708
8475
|
try {
|
|
7709
8476
|
const envs = listEnvironments(opts.project);
|
|
7710
8477
|
if (envs.length === 0) {
|
|
7711
|
-
|
|
8478
|
+
log(chalk5.dim("No environments configured. Add one with: testers env add <name> --url <url>"));
|
|
7712
8479
|
return;
|
|
7713
8480
|
}
|
|
7714
8481
|
for (const env of envs) {
|
|
7715
8482
|
const marker = env.isDefault ? chalk5.green(" \u2605 default") : "";
|
|
7716
8483
|
const auth = env.authPresetName ? chalk5.dim(` (auth: ${env.authPresetName})`) : "";
|
|
7717
|
-
|
|
8484
|
+
log(` ${chalk5.bold(env.name)} ${env.url}${auth}${marker}`);
|
|
7718
8485
|
}
|
|
7719
8486
|
} catch (error) {
|
|
7720
|
-
|
|
8487
|
+
logError(chalk5.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
7721
8488
|
process.exit(1);
|
|
7722
8489
|
}
|
|
7723
8490
|
});
|
|
@@ -7725,9 +8492,9 @@ envCmd.command("use <name>").description("Set an environment as the default").ac
|
|
|
7725
8492
|
try {
|
|
7726
8493
|
setDefaultEnvironment(name);
|
|
7727
8494
|
const env = getEnvironment(name);
|
|
7728
|
-
|
|
8495
|
+
log(chalk5.green(`Default environment set: ${env.name} \u2192 ${env.url}`));
|
|
7729
8496
|
} catch (error) {
|
|
7730
|
-
|
|
8497
|
+
logError(chalk5.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
7731
8498
|
process.exit(1);
|
|
7732
8499
|
}
|
|
7733
8500
|
});
|
|
@@ -7735,13 +8502,13 @@ envCmd.command("delete <name>").description("Delete an environment").action((nam
|
|
|
7735
8502
|
try {
|
|
7736
8503
|
const deleted = deleteEnvironment(name);
|
|
7737
8504
|
if (deleted) {
|
|
7738
|
-
|
|
8505
|
+
log(chalk5.green(`Environment deleted: ${name}`));
|
|
7739
8506
|
} else {
|
|
7740
|
-
|
|
8507
|
+
logError(chalk5.red(`Environment not found: ${name}`));
|
|
7741
8508
|
process.exit(1);
|
|
7742
8509
|
}
|
|
7743
8510
|
} catch (error) {
|
|
7744
|
-
|
|
8511
|
+
logError(chalk5.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
7745
8512
|
process.exit(1);
|
|
7746
8513
|
}
|
|
7747
8514
|
});
|
|
@@ -7749,9 +8516,9 @@ program2.command("baseline <run-id>").description("Set a run as the visual basel
|
|
|
7749
8516
|
try {
|
|
7750
8517
|
setBaseline(runId);
|
|
7751
8518
|
const run = getRun(runId);
|
|
7752
|
-
|
|
8519
|
+
log(chalk5.green(`Baseline set: ${chalk5.bold(runId.slice(0, 8))}${run ? ` (${run.status}, ${run.total} scenarios)` : ""}`));
|
|
7753
8520
|
} catch (error) {
|
|
7754
|
-
|
|
8521
|
+
logError(chalk5.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
7755
8522
|
process.exit(1);
|
|
7756
8523
|
}
|
|
7757
8524
|
});
|
|
@@ -7759,29 +8526,99 @@ program2.command("import-api <spec>").description("Import test scenarios from an
|
|
|
7759
8526
|
try {
|
|
7760
8527
|
const { importFromOpenAPI: importFromOpenAPI2 } = await Promise.resolve().then(() => (init_openapi_import(), exports_openapi_import));
|
|
7761
8528
|
const { imported, scenarios } = importFromOpenAPI2(spec, resolveProject(opts.project) ?? undefined);
|
|
7762
|
-
|
|
8529
|
+
log(chalk5.green(`
|
|
7763
8530
|
Imported ${imported} scenarios from API spec:`));
|
|
7764
8531
|
for (const s of scenarios) {
|
|
7765
|
-
|
|
8532
|
+
log(` ${chalk5.cyan(s.shortId)} ${s.name} ${chalk5.dim(`[${s.tags.join(", ")}]`)}`);
|
|
7766
8533
|
}
|
|
7767
|
-
|
|
8534
|
+
log("");
|
|
7768
8535
|
} catch (error) {
|
|
7769
|
-
|
|
8536
|
+
logError(chalk5.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
7770
8537
|
process.exit(1);
|
|
7771
8538
|
}
|
|
7772
8539
|
});
|
|
7773
8540
|
program2.command("record <url>").description("Record a browser session and generate a test scenario").option("-n, --name <name>", "Scenario name", "Recorded session").option("--project <id>", "Project ID").action(async (url, opts) => {
|
|
7774
8541
|
try {
|
|
7775
8542
|
const { recordAndSave: recordAndSave2 } = await Promise.resolve().then(() => (init_recorder(), exports_recorder));
|
|
7776
|
-
|
|
8543
|
+
log(chalk5.blue("Opening browser for recording..."));
|
|
7777
8544
|
const { recording, scenario } = await recordAndSave2(url, opts.name, resolveProject(opts.project) ?? undefined);
|
|
7778
|
-
|
|
7779
|
-
|
|
7780
|
-
|
|
7781
|
-
|
|
8545
|
+
log("");
|
|
8546
|
+
log(chalk5.green(`Recording saved as scenario ${chalk5.bold(scenario.shortId)}: ${scenario.name}`));
|
|
8547
|
+
log(chalk5.dim(` ${recording.actions.length} actions recorded in ${(recording.duration / 1000).toFixed(0)}s`));
|
|
8548
|
+
log(chalk5.dim(` ${scenario.steps.length} steps generated`));
|
|
7782
8549
|
} catch (error) {
|
|
7783
|
-
|
|
8550
|
+
logError(chalk5.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
7784
8551
|
process.exit(1);
|
|
7785
8552
|
}
|
|
7786
8553
|
});
|
|
8554
|
+
program2.command("doctor").description("Check system setup and configuration").action(async () => {
|
|
8555
|
+
let allPassed = true;
|
|
8556
|
+
const hasApiKey = Boolean(process.env["ANTHROPIC_API_KEY"]);
|
|
8557
|
+
if (hasApiKey) {
|
|
8558
|
+
log(chalk5.green("\u2713") + " ANTHROPIC_API_KEY is set");
|
|
8559
|
+
} else {
|
|
8560
|
+
log(chalk5.red("\u2717") + " ANTHROPIC_API_KEY is not set (required for AI-powered tests)");
|
|
8561
|
+
allPassed = false;
|
|
8562
|
+
}
|
|
8563
|
+
const dbPath = join6(process.env["HOME"] ?? "~", ".testers", "testers.db");
|
|
8564
|
+
try {
|
|
8565
|
+
const { Database: Database3 } = await import("bun:sqlite");
|
|
8566
|
+
const db2 = new Database3(dbPath, { create: true });
|
|
8567
|
+
db2.close();
|
|
8568
|
+
log(chalk5.green("\u2713") + ` Database accessible: ${dbPath}`);
|
|
8569
|
+
} catch (err) {
|
|
8570
|
+
log(chalk5.red("\u2717") + ` Database not accessible at ${dbPath}: ${err instanceof Error ? err.message : String(err)}`);
|
|
8571
|
+
allPassed = false;
|
|
8572
|
+
}
|
|
8573
|
+
try {
|
|
8574
|
+
const { chromium: chromium4 } = await import("playwright");
|
|
8575
|
+
const execPath = chromium4.executablePath();
|
|
8576
|
+
const { existsSync: fsExists } = await import("fs");
|
|
8577
|
+
if (fsExists(execPath)) {
|
|
8578
|
+
log(chalk5.green("\u2713") + " Playwright chromium is installed");
|
|
8579
|
+
} else {
|
|
8580
|
+
log(chalk5.red("\u2717") + ` Playwright chromium executable not found at ${execPath}. Run: testers install`);
|
|
8581
|
+
allPassed = false;
|
|
8582
|
+
}
|
|
8583
|
+
} catch {
|
|
8584
|
+
log(chalk5.red("\u2717") + " Playwright is not installed. Run: testers install");
|
|
8585
|
+
allPassed = false;
|
|
8586
|
+
}
|
|
8587
|
+
if (!allPassed) {
|
|
8588
|
+
process.exit(1);
|
|
8589
|
+
}
|
|
8590
|
+
});
|
|
8591
|
+
program2.command("serve").description("Start the Open Testers web dashboard").option("--no-open", "Do not open the browser after starting", false).option("--port <port>", "Port to listen on", "19450").action(async (opts) => {
|
|
8592
|
+
try {
|
|
8593
|
+
const port = parseInt(opts.port, 10);
|
|
8594
|
+
const url = `http://localhost:${port}`;
|
|
8595
|
+
const serverBin = join6(resolve(process.execPath, ".."), "..", "dist", "server", "index.js");
|
|
8596
|
+
const { join: pathJoin, resolve: pathResolve, dirname: dirname2 } = await import("path");
|
|
8597
|
+
const { fileURLToPath } = await import("url");
|
|
8598
|
+
const serverPath = pathJoin(dirname2(fileURLToPath(import.meta.url)), "..", "server", "index.js");
|
|
8599
|
+
const proc = Bun.spawn(["bun", "run", serverPath], {
|
|
8600
|
+
env: { ...process.env, TESTERS_PORT: String(port) },
|
|
8601
|
+
stdout: "inherit",
|
|
8602
|
+
stderr: "inherit"
|
|
8603
|
+
});
|
|
8604
|
+
log(chalk5.green(`Open Testers dashboard starting at ${url}`));
|
|
8605
|
+
if (opts.open !== false) {
|
|
8606
|
+
await new Promise((r) => setTimeout(r, 1500));
|
|
8607
|
+
const openCmd = process.platform === "darwin" ? "open" : "xdg-open";
|
|
8608
|
+
Bun.spawn([openCmd, url]);
|
|
8609
|
+
}
|
|
8610
|
+
await proc.exited;
|
|
8611
|
+
} catch (error) {
|
|
8612
|
+
logError(chalk5.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
8613
|
+
process.exit(1);
|
|
8614
|
+
}
|
|
8615
|
+
});
|
|
8616
|
+
program2.hook("preAction", () => {
|
|
8617
|
+
const opts = program2.opts();
|
|
8618
|
+
QUIET = opts.quiet === true;
|
|
8619
|
+
NO_COLOR = opts.color === false || process.env["FORCE_COLOR"] === "0";
|
|
8620
|
+
if (NO_COLOR) {
|
|
8621
|
+
process.env["FORCE_COLOR"] = "0";
|
|
8622
|
+
}
|
|
8623
|
+
});
|
|
7787
8624
|
program2.parse();
|