@hasna/testers 0.0.5 → 0.0.7
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/dist/cli/index.js +1580 -342
- package/dist/db/database.d.ts.map +1 -1
- package/dist/db/environments.d.ts +22 -0
- package/dist/db/environments.d.ts.map +1 -0
- package/dist/db/flows.d.ts +12 -0
- package/dist/db/flows.d.ts.map +1 -0
- package/dist/db/runs.d.ts.map +1 -1
- package/dist/db/scenarios.d.ts.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +481 -165
- package/dist/lib/assertions.d.ts +26 -0
- package/dist/lib/assertions.d.ts.map +1 -0
- package/dist/lib/ci.d.ts +2 -0
- package/dist/lib/ci.d.ts.map +1 -0
- package/dist/lib/openapi-import.d.ts +8 -0
- package/dist/lib/openapi-import.d.ts.map +1 -0
- package/dist/lib/recorder.d.ts +24 -0
- package/dist/lib/recorder.d.ts.map +1 -0
- package/dist/lib/runner.d.ts.map +1 -1
- package/dist/lib/visual-diff.d.ts +36 -0
- package/dist/lib/visual-diff.d.ts.map +1 -0
- package/dist/mcp/index.js +3102 -2693
- package/dist/server/index.js +500 -88
- package/dist/types/index.d.ts +44 -0
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -2090,6 +2090,7 @@ function scenarioFromRow(row) {
|
|
|
2090
2090
|
requiresAuth: row.requires_auth === 1,
|
|
2091
2091
|
authConfig: row.auth_config ? JSON.parse(row.auth_config) : null,
|
|
2092
2092
|
metadata: row.metadata ? JSON.parse(row.metadata) : null,
|
|
2093
|
+
assertions: JSON.parse(row.assertions || "[]"),
|
|
2093
2094
|
version: row.version,
|
|
2094
2095
|
createdAt: row.created_at,
|
|
2095
2096
|
updatedAt: row.updated_at
|
|
@@ -2109,7 +2110,8 @@ function runFromRow(row) {
|
|
|
2109
2110
|
failed: row.failed,
|
|
2110
2111
|
startedAt: row.started_at,
|
|
2111
2112
|
finishedAt: row.finished_at,
|
|
2112
|
-
metadata: row.metadata ? JSON.parse(row.metadata) : null
|
|
2113
|
+
metadata: row.metadata ? JSON.parse(row.metadata) : null,
|
|
2114
|
+
isBaseline: row.is_baseline === 1
|
|
2113
2115
|
};
|
|
2114
2116
|
}
|
|
2115
2117
|
function resultFromRow(row) {
|
|
@@ -2165,7 +2167,18 @@ function scheduleFromRow(row) {
|
|
|
2165
2167
|
updatedAt: row.updated_at
|
|
2166
2168
|
};
|
|
2167
2169
|
}
|
|
2168
|
-
|
|
2170
|
+
function flowFromRow(row) {
|
|
2171
|
+
return {
|
|
2172
|
+
id: row.id,
|
|
2173
|
+
projectId: row.project_id,
|
|
2174
|
+
name: row.name,
|
|
2175
|
+
description: row.description,
|
|
2176
|
+
scenarioIds: JSON.parse(row.scenario_ids),
|
|
2177
|
+
createdAt: row.created_at,
|
|
2178
|
+
updatedAt: row.updated_at
|
|
2179
|
+
};
|
|
2180
|
+
}
|
|
2181
|
+
var MODEL_MAP, VersionConflictError, BrowserError, AIClientError, TodosConnectionError, ScheduleNotFoundError, DependencyCycleError;
|
|
2169
2182
|
var init_types = __esm(() => {
|
|
2170
2183
|
MODEL_MAP = {
|
|
2171
2184
|
quick: "claude-haiku-4-5-20251001",
|
|
@@ -2202,6 +2215,12 @@ var init_types = __esm(() => {
|
|
|
2202
2215
|
this.name = "ScheduleNotFoundError";
|
|
2203
2216
|
}
|
|
2204
2217
|
};
|
|
2218
|
+
DependencyCycleError = class DependencyCycleError extends Error {
|
|
2219
|
+
constructor(scenarioId, dependsOn) {
|
|
2220
|
+
super(`Adding dependency ${dependsOn} to ${scenarioId} would create a cycle`);
|
|
2221
|
+
this.name = "DependencyCycleError";
|
|
2222
|
+
}
|
|
2223
|
+
};
|
|
2205
2224
|
});
|
|
2206
2225
|
|
|
2207
2226
|
// src/db/database.ts
|
|
@@ -2425,166 +2444,51 @@ var init_database = __esm(() => {
|
|
|
2425
2444
|
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
2426
2445
|
);
|
|
2427
2446
|
CREATE INDEX IF NOT EXISTS idx_webhooks_active ON webhooks(active);
|
|
2447
|
+
`,
|
|
2448
|
+
`
|
|
2449
|
+
CREATE TABLE IF NOT EXISTS scenario_dependencies (
|
|
2450
|
+
scenario_id TEXT NOT NULL REFERENCES scenarios(id) ON DELETE CASCADE,
|
|
2451
|
+
depends_on TEXT NOT NULL REFERENCES scenarios(id) ON DELETE CASCADE,
|
|
2452
|
+
PRIMARY KEY (scenario_id, depends_on),
|
|
2453
|
+
CHECK (scenario_id != depends_on)
|
|
2454
|
+
);
|
|
2455
|
+
|
|
2456
|
+
CREATE TABLE IF NOT EXISTS flows (
|
|
2457
|
+
id TEXT PRIMARY KEY,
|
|
2458
|
+
project_id TEXT REFERENCES projects(id) ON DELETE CASCADE,
|
|
2459
|
+
name TEXT NOT NULL,
|
|
2460
|
+
description TEXT,
|
|
2461
|
+
scenario_ids TEXT NOT NULL DEFAULT '[]',
|
|
2462
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
2463
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
2464
|
+
);
|
|
2465
|
+
|
|
2466
|
+
CREATE INDEX IF NOT EXISTS idx_deps_scenario ON scenario_dependencies(scenario_id);
|
|
2467
|
+
CREATE INDEX IF NOT EXISTS idx_deps_depends ON scenario_dependencies(depends_on);
|
|
2468
|
+
CREATE INDEX IF NOT EXISTS idx_flows_project ON flows(project_id);
|
|
2469
|
+
`,
|
|
2470
|
+
`
|
|
2471
|
+
ALTER TABLE scenarios ADD COLUMN assertions TEXT DEFAULT '[]';
|
|
2472
|
+
`,
|
|
2473
|
+
`
|
|
2474
|
+
CREATE TABLE IF NOT EXISTS environments (
|
|
2475
|
+
id TEXT PRIMARY KEY,
|
|
2476
|
+
name TEXT NOT NULL UNIQUE,
|
|
2477
|
+
url TEXT NOT NULL,
|
|
2478
|
+
auth_preset_name TEXT,
|
|
2479
|
+
project_id TEXT REFERENCES projects(id) ON DELETE CASCADE,
|
|
2480
|
+
is_default INTEGER NOT NULL DEFAULT 0,
|
|
2481
|
+
metadata TEXT DEFAULT '{}',
|
|
2482
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
2483
|
+
);
|
|
2484
|
+
`,
|
|
2485
|
+
`
|
|
2486
|
+
ALTER TABLE runs ADD COLUMN is_baseline INTEGER NOT NULL DEFAULT 0;
|
|
2428
2487
|
`
|
|
2429
2488
|
];
|
|
2430
2489
|
});
|
|
2431
2490
|
|
|
2432
|
-
// src/db/runs.ts
|
|
2433
|
-
var exports_runs = {};
|
|
2434
|
-
__export(exports_runs, {
|
|
2435
|
-
updateRun: () => updateRun,
|
|
2436
|
-
listRuns: () => listRuns,
|
|
2437
|
-
getRun: () => getRun,
|
|
2438
|
-
deleteRun: () => deleteRun,
|
|
2439
|
-
createRun: () => createRun
|
|
2440
|
-
});
|
|
2441
|
-
function createRun(input) {
|
|
2442
|
-
const db2 = getDatabase();
|
|
2443
|
-
const id = uuid();
|
|
2444
|
-
const timestamp = now();
|
|
2445
|
-
db2.query(`
|
|
2446
|
-
INSERT INTO runs (id, project_id, status, url, model, headed, parallel, total, passed, failed, started_at, finished_at, metadata)
|
|
2447
|
-
VALUES (?, ?, 'pending', ?, ?, ?, ?, 0, 0, 0, ?, NULL, ?)
|
|
2448
|
-
`).run(id, input.projectId ?? null, input.url, input.model, input.headed ? 1 : 0, input.parallel ?? 1, timestamp, input.model ? JSON.stringify({}) : null);
|
|
2449
|
-
return getRun(id);
|
|
2450
|
-
}
|
|
2451
|
-
function getRun(id) {
|
|
2452
|
-
const db2 = getDatabase();
|
|
2453
|
-
let row = db2.query("SELECT * FROM runs WHERE id = ?").get(id);
|
|
2454
|
-
if (row)
|
|
2455
|
-
return runFromRow(row);
|
|
2456
|
-
const fullId = resolvePartialId("runs", id);
|
|
2457
|
-
if (fullId) {
|
|
2458
|
-
row = db2.query("SELECT * FROM runs WHERE id = ?").get(fullId);
|
|
2459
|
-
if (row)
|
|
2460
|
-
return runFromRow(row);
|
|
2461
|
-
}
|
|
2462
|
-
return null;
|
|
2463
|
-
}
|
|
2464
|
-
function listRuns(filter) {
|
|
2465
|
-
const db2 = getDatabase();
|
|
2466
|
-
const conditions = [];
|
|
2467
|
-
const params = [];
|
|
2468
|
-
if (filter?.projectId) {
|
|
2469
|
-
conditions.push("project_id = ?");
|
|
2470
|
-
params.push(filter.projectId);
|
|
2471
|
-
}
|
|
2472
|
-
if (filter?.status) {
|
|
2473
|
-
conditions.push("status = ?");
|
|
2474
|
-
params.push(filter.status);
|
|
2475
|
-
}
|
|
2476
|
-
let sql = "SELECT * FROM runs";
|
|
2477
|
-
if (conditions.length > 0) {
|
|
2478
|
-
sql += " WHERE " + conditions.join(" AND ");
|
|
2479
|
-
}
|
|
2480
|
-
sql += " ORDER BY started_at DESC";
|
|
2481
|
-
if (filter?.limit) {
|
|
2482
|
-
sql += " LIMIT ?";
|
|
2483
|
-
params.push(filter.limit);
|
|
2484
|
-
}
|
|
2485
|
-
if (filter?.offset) {
|
|
2486
|
-
sql += " OFFSET ?";
|
|
2487
|
-
params.push(filter.offset);
|
|
2488
|
-
}
|
|
2489
|
-
const rows = db2.query(sql).all(...params);
|
|
2490
|
-
return rows.map(runFromRow);
|
|
2491
|
-
}
|
|
2492
|
-
function updateRun(id, updates) {
|
|
2493
|
-
const db2 = getDatabase();
|
|
2494
|
-
const existing = getRun(id);
|
|
2495
|
-
if (!existing) {
|
|
2496
|
-
throw new Error(`Run not found: ${id}`);
|
|
2497
|
-
}
|
|
2498
|
-
const sets = [];
|
|
2499
|
-
const params = [];
|
|
2500
|
-
if (updates.status !== undefined) {
|
|
2501
|
-
sets.push("status = ?");
|
|
2502
|
-
params.push(updates.status);
|
|
2503
|
-
}
|
|
2504
|
-
if (updates.url !== undefined) {
|
|
2505
|
-
sets.push("url = ?");
|
|
2506
|
-
params.push(updates.url);
|
|
2507
|
-
}
|
|
2508
|
-
if (updates.model !== undefined) {
|
|
2509
|
-
sets.push("model = ?");
|
|
2510
|
-
params.push(updates.model);
|
|
2511
|
-
}
|
|
2512
|
-
if (updates.headed !== undefined) {
|
|
2513
|
-
sets.push("headed = ?");
|
|
2514
|
-
params.push(updates.headed);
|
|
2515
|
-
}
|
|
2516
|
-
if (updates.parallel !== undefined) {
|
|
2517
|
-
sets.push("parallel = ?");
|
|
2518
|
-
params.push(updates.parallel);
|
|
2519
|
-
}
|
|
2520
|
-
if (updates.total !== undefined) {
|
|
2521
|
-
sets.push("total = ?");
|
|
2522
|
-
params.push(updates.total);
|
|
2523
|
-
}
|
|
2524
|
-
if (updates.passed !== undefined) {
|
|
2525
|
-
sets.push("passed = ?");
|
|
2526
|
-
params.push(updates.passed);
|
|
2527
|
-
}
|
|
2528
|
-
if (updates.failed !== undefined) {
|
|
2529
|
-
sets.push("failed = ?");
|
|
2530
|
-
params.push(updates.failed);
|
|
2531
|
-
}
|
|
2532
|
-
if (updates.started_at !== undefined) {
|
|
2533
|
-
sets.push("started_at = ?");
|
|
2534
|
-
params.push(updates.started_at);
|
|
2535
|
-
}
|
|
2536
|
-
if (updates.finished_at !== undefined) {
|
|
2537
|
-
sets.push("finished_at = ?");
|
|
2538
|
-
params.push(updates.finished_at);
|
|
2539
|
-
}
|
|
2540
|
-
if (updates.metadata !== undefined) {
|
|
2541
|
-
sets.push("metadata = ?");
|
|
2542
|
-
params.push(updates.metadata);
|
|
2543
|
-
}
|
|
2544
|
-
if (sets.length === 0) {
|
|
2545
|
-
return existing;
|
|
2546
|
-
}
|
|
2547
|
-
params.push(existing.id);
|
|
2548
|
-
db2.query(`UPDATE runs SET ${sets.join(", ")} WHERE id = ?`).run(...params);
|
|
2549
|
-
return getRun(existing.id);
|
|
2550
|
-
}
|
|
2551
|
-
function deleteRun(id) {
|
|
2552
|
-
const db2 = getDatabase();
|
|
2553
|
-
const run = getRun(id);
|
|
2554
|
-
if (!run)
|
|
2555
|
-
return false;
|
|
2556
|
-
const result = db2.query("DELETE FROM runs WHERE id = ?").run(run.id);
|
|
2557
|
-
return result.changes > 0;
|
|
2558
|
-
}
|
|
2559
|
-
var init_runs = __esm(() => {
|
|
2560
|
-
init_types();
|
|
2561
|
-
init_database();
|
|
2562
|
-
});
|
|
2563
|
-
|
|
2564
|
-
// node_modules/commander/esm.mjs
|
|
2565
|
-
var import__ = __toESM(require_commander(), 1);
|
|
2566
|
-
var {
|
|
2567
|
-
program,
|
|
2568
|
-
createCommand,
|
|
2569
|
-
createArgument,
|
|
2570
|
-
createOption,
|
|
2571
|
-
CommanderError,
|
|
2572
|
-
InvalidArgumentError,
|
|
2573
|
-
InvalidOptionArgumentError,
|
|
2574
|
-
Command,
|
|
2575
|
-
Argument,
|
|
2576
|
-
Option,
|
|
2577
|
-
Help
|
|
2578
|
-
} = import__.default;
|
|
2579
|
-
|
|
2580
|
-
// src/cli/index.tsx
|
|
2581
|
-
import chalk4 from "chalk";
|
|
2582
|
-
import { readFileSync as readFileSync4, readdirSync, writeFileSync as writeFileSync3 } from "fs";
|
|
2583
|
-
import { join as join6, resolve } from "path";
|
|
2584
|
-
|
|
2585
2491
|
// src/db/scenarios.ts
|
|
2586
|
-
init_types();
|
|
2587
|
-
init_database();
|
|
2588
2492
|
function nextShortId(projectId) {
|
|
2589
2493
|
const db2 = getDatabase();
|
|
2590
2494
|
if (projectId) {
|
|
@@ -2603,9 +2507,9 @@ function createScenario(input) {
|
|
|
2603
2507
|
const short_id = nextShortId(input.projectId);
|
|
2604
2508
|
const timestamp = now();
|
|
2605
2509
|
db2.query(`
|
|
2606
|
-
INSERT INTO scenarios (id, short_id, project_id, name, description, steps, tags, priority, model, timeout_ms, target_path, requires_auth, auth_config, metadata, version, created_at, updated_at)
|
|
2607
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?, ?)
|
|
2608
|
-
`).run(id, short_id, input.projectId ?? null, input.name, input.description, JSON.stringify(input.steps ?? []), JSON.stringify(input.tags ?? []), input.priority ?? "medium", input.model ?? null, input.timeoutMs ?? null, input.targetPath ?? null, input.requiresAuth ? 1 : 0, input.authConfig ? JSON.stringify(input.authConfig) : null, input.metadata ? JSON.stringify(input.metadata) : null, timestamp, timestamp);
|
|
2510
|
+
INSERT INTO scenarios (id, short_id, project_id, name, description, steps, tags, priority, model, timeout_ms, target_path, requires_auth, auth_config, metadata, assertions, version, created_at, updated_at)
|
|
2511
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?, ?)
|
|
2512
|
+
`).run(id, short_id, input.projectId ?? null, input.name, input.description, JSON.stringify(input.steps ?? []), JSON.stringify(input.tags ?? []), input.priority ?? "medium", input.model ?? null, input.timeoutMs ?? null, input.targetPath ?? null, input.requiresAuth ? 1 : 0, input.authConfig ? JSON.stringify(input.authConfig) : null, input.metadata ? JSON.stringify(input.metadata) : null, JSON.stringify(input.assertions ?? []), timestamp, timestamp);
|
|
2609
2513
|
return getScenario(id);
|
|
2610
2514
|
}
|
|
2611
2515
|
function getScenario(id) {
|
|
@@ -2723,6 +2627,10 @@ function updateScenario(id, input, version) {
|
|
|
2723
2627
|
sets.push("metadata = ?");
|
|
2724
2628
|
params.push(JSON.stringify(input.metadata));
|
|
2725
2629
|
}
|
|
2630
|
+
if (input.assertions !== undefined) {
|
|
2631
|
+
sets.push("assertions = ?");
|
|
2632
|
+
params.push(JSON.stringify(input.assertions));
|
|
2633
|
+
}
|
|
2726
2634
|
if (sets.length === 0) {
|
|
2727
2635
|
return existing;
|
|
2728
2636
|
}
|
|
@@ -2746,9 +2654,579 @@ function deleteScenario(id) {
|
|
|
2746
2654
|
const result = db2.query("DELETE FROM scenarios WHERE id = ?").run(scenario.id);
|
|
2747
2655
|
return result.changes > 0;
|
|
2748
2656
|
}
|
|
2657
|
+
var init_scenarios = __esm(() => {
|
|
2658
|
+
init_types();
|
|
2659
|
+
init_database();
|
|
2660
|
+
});
|
|
2661
|
+
|
|
2662
|
+
// src/db/runs.ts
|
|
2663
|
+
var exports_runs = {};
|
|
2664
|
+
__export(exports_runs, {
|
|
2665
|
+
updateRun: () => updateRun,
|
|
2666
|
+
listRuns: () => listRuns,
|
|
2667
|
+
getRun: () => getRun,
|
|
2668
|
+
deleteRun: () => deleteRun,
|
|
2669
|
+
createRun: () => createRun
|
|
2670
|
+
});
|
|
2671
|
+
function createRun(input) {
|
|
2672
|
+
const db2 = getDatabase();
|
|
2673
|
+
const id = uuid();
|
|
2674
|
+
const timestamp = now();
|
|
2675
|
+
db2.query(`
|
|
2676
|
+
INSERT INTO runs (id, project_id, status, url, model, headed, parallel, total, passed, failed, started_at, finished_at, metadata)
|
|
2677
|
+
VALUES (?, ?, 'pending', ?, ?, ?, ?, 0, 0, 0, ?, NULL, ?)
|
|
2678
|
+
`).run(id, input.projectId ?? null, input.url, input.model, input.headed ? 1 : 0, input.parallel ?? 1, timestamp, input.model ? JSON.stringify({}) : null);
|
|
2679
|
+
return getRun(id);
|
|
2680
|
+
}
|
|
2681
|
+
function getRun(id) {
|
|
2682
|
+
const db2 = getDatabase();
|
|
2683
|
+
let row = db2.query("SELECT * FROM runs WHERE id = ?").get(id);
|
|
2684
|
+
if (row)
|
|
2685
|
+
return runFromRow(row);
|
|
2686
|
+
const fullId = resolvePartialId("runs", id);
|
|
2687
|
+
if (fullId) {
|
|
2688
|
+
row = db2.query("SELECT * FROM runs WHERE id = ?").get(fullId);
|
|
2689
|
+
if (row)
|
|
2690
|
+
return runFromRow(row);
|
|
2691
|
+
}
|
|
2692
|
+
return null;
|
|
2693
|
+
}
|
|
2694
|
+
function listRuns(filter) {
|
|
2695
|
+
const db2 = getDatabase();
|
|
2696
|
+
const conditions = [];
|
|
2697
|
+
const params = [];
|
|
2698
|
+
if (filter?.projectId) {
|
|
2699
|
+
conditions.push("project_id = ?");
|
|
2700
|
+
params.push(filter.projectId);
|
|
2701
|
+
}
|
|
2702
|
+
if (filter?.status) {
|
|
2703
|
+
conditions.push("status = ?");
|
|
2704
|
+
params.push(filter.status);
|
|
2705
|
+
}
|
|
2706
|
+
let sql = "SELECT * FROM runs";
|
|
2707
|
+
if (conditions.length > 0) {
|
|
2708
|
+
sql += " WHERE " + conditions.join(" AND ");
|
|
2709
|
+
}
|
|
2710
|
+
sql += " ORDER BY started_at DESC";
|
|
2711
|
+
if (filter?.limit) {
|
|
2712
|
+
sql += " LIMIT ?";
|
|
2713
|
+
params.push(filter.limit);
|
|
2714
|
+
}
|
|
2715
|
+
if (filter?.offset) {
|
|
2716
|
+
sql += " OFFSET ?";
|
|
2717
|
+
params.push(filter.offset);
|
|
2718
|
+
}
|
|
2719
|
+
const rows = db2.query(sql).all(...params);
|
|
2720
|
+
return rows.map(runFromRow);
|
|
2721
|
+
}
|
|
2722
|
+
function updateRun(id, updates) {
|
|
2723
|
+
const db2 = getDatabase();
|
|
2724
|
+
const existing = getRun(id);
|
|
2725
|
+
if (!existing) {
|
|
2726
|
+
throw new Error(`Run not found: ${id}`);
|
|
2727
|
+
}
|
|
2728
|
+
const sets = [];
|
|
2729
|
+
const params = [];
|
|
2730
|
+
if (updates.status !== undefined) {
|
|
2731
|
+
sets.push("status = ?");
|
|
2732
|
+
params.push(updates.status);
|
|
2733
|
+
}
|
|
2734
|
+
if (updates.url !== undefined) {
|
|
2735
|
+
sets.push("url = ?");
|
|
2736
|
+
params.push(updates.url);
|
|
2737
|
+
}
|
|
2738
|
+
if (updates.model !== undefined) {
|
|
2739
|
+
sets.push("model = ?");
|
|
2740
|
+
params.push(updates.model);
|
|
2741
|
+
}
|
|
2742
|
+
if (updates.headed !== undefined) {
|
|
2743
|
+
sets.push("headed = ?");
|
|
2744
|
+
params.push(updates.headed);
|
|
2745
|
+
}
|
|
2746
|
+
if (updates.parallel !== undefined) {
|
|
2747
|
+
sets.push("parallel = ?");
|
|
2748
|
+
params.push(updates.parallel);
|
|
2749
|
+
}
|
|
2750
|
+
if (updates.total !== undefined) {
|
|
2751
|
+
sets.push("total = ?");
|
|
2752
|
+
params.push(updates.total);
|
|
2753
|
+
}
|
|
2754
|
+
if (updates.passed !== undefined) {
|
|
2755
|
+
sets.push("passed = ?");
|
|
2756
|
+
params.push(updates.passed);
|
|
2757
|
+
}
|
|
2758
|
+
if (updates.failed !== undefined) {
|
|
2759
|
+
sets.push("failed = ?");
|
|
2760
|
+
params.push(updates.failed);
|
|
2761
|
+
}
|
|
2762
|
+
if (updates.started_at !== undefined) {
|
|
2763
|
+
sets.push("started_at = ?");
|
|
2764
|
+
params.push(updates.started_at);
|
|
2765
|
+
}
|
|
2766
|
+
if (updates.finished_at !== undefined) {
|
|
2767
|
+
sets.push("finished_at = ?");
|
|
2768
|
+
params.push(updates.finished_at);
|
|
2769
|
+
}
|
|
2770
|
+
if (updates.metadata !== undefined) {
|
|
2771
|
+
sets.push("metadata = ?");
|
|
2772
|
+
params.push(updates.metadata);
|
|
2773
|
+
}
|
|
2774
|
+
if (updates.is_baseline !== undefined) {
|
|
2775
|
+
sets.push("is_baseline = ?");
|
|
2776
|
+
params.push(updates.is_baseline);
|
|
2777
|
+
}
|
|
2778
|
+
if (sets.length === 0) {
|
|
2779
|
+
return existing;
|
|
2780
|
+
}
|
|
2781
|
+
params.push(existing.id);
|
|
2782
|
+
db2.query(`UPDATE runs SET ${sets.join(", ")} WHERE id = ?`).run(...params);
|
|
2783
|
+
return getRun(existing.id);
|
|
2784
|
+
}
|
|
2785
|
+
function deleteRun(id) {
|
|
2786
|
+
const db2 = getDatabase();
|
|
2787
|
+
const run = getRun(id);
|
|
2788
|
+
if (!run)
|
|
2789
|
+
return false;
|
|
2790
|
+
const result = db2.query("DELETE FROM runs WHERE id = ?").run(run.id);
|
|
2791
|
+
return result.changes > 0;
|
|
2792
|
+
}
|
|
2793
|
+
var init_runs = __esm(() => {
|
|
2794
|
+
init_types();
|
|
2795
|
+
init_database();
|
|
2796
|
+
});
|
|
2797
|
+
|
|
2798
|
+
// src/db/flows.ts
|
|
2799
|
+
var exports_flows = {};
|
|
2800
|
+
__export(exports_flows, {
|
|
2801
|
+
topologicalSort: () => topologicalSort,
|
|
2802
|
+
removeDependency: () => removeDependency,
|
|
2803
|
+
listFlows: () => listFlows,
|
|
2804
|
+
getTransitiveDependencies: () => getTransitiveDependencies,
|
|
2805
|
+
getFlow: () => getFlow,
|
|
2806
|
+
getDependents: () => getDependents,
|
|
2807
|
+
getDependencies: () => getDependencies,
|
|
2808
|
+
deleteFlow: () => deleteFlow,
|
|
2809
|
+
createFlow: () => createFlow,
|
|
2810
|
+
addDependency: () => addDependency
|
|
2811
|
+
});
|
|
2812
|
+
function addDependency(scenarioId, dependsOn) {
|
|
2813
|
+
const db2 = getDatabase();
|
|
2814
|
+
const visited = new Set;
|
|
2815
|
+
const queue = [dependsOn];
|
|
2816
|
+
while (queue.length > 0) {
|
|
2817
|
+
const current = queue.shift();
|
|
2818
|
+
if (current === scenarioId) {
|
|
2819
|
+
throw new DependencyCycleError(scenarioId, dependsOn);
|
|
2820
|
+
}
|
|
2821
|
+
if (visited.has(current))
|
|
2822
|
+
continue;
|
|
2823
|
+
visited.add(current);
|
|
2824
|
+
const deps = db2.query("SELECT depends_on FROM scenario_dependencies WHERE scenario_id = ?").all(current);
|
|
2825
|
+
for (const dep of deps) {
|
|
2826
|
+
if (!visited.has(dep.depends_on)) {
|
|
2827
|
+
queue.push(dep.depends_on);
|
|
2828
|
+
}
|
|
2829
|
+
}
|
|
2830
|
+
}
|
|
2831
|
+
db2.query("INSERT OR IGNORE INTO scenario_dependencies (scenario_id, depends_on) VALUES (?, ?)").run(scenarioId, dependsOn);
|
|
2832
|
+
}
|
|
2833
|
+
function removeDependency(scenarioId, dependsOn) {
|
|
2834
|
+
const db2 = getDatabase();
|
|
2835
|
+
const result = db2.query("DELETE FROM scenario_dependencies WHERE scenario_id = ? AND depends_on = ?").run(scenarioId, dependsOn);
|
|
2836
|
+
return result.changes > 0;
|
|
2837
|
+
}
|
|
2838
|
+
function getDependencies(scenarioId) {
|
|
2839
|
+
const db2 = getDatabase();
|
|
2840
|
+
const rows = db2.query("SELECT depends_on FROM scenario_dependencies WHERE scenario_id = ?").all(scenarioId);
|
|
2841
|
+
return rows.map((r) => r.depends_on);
|
|
2842
|
+
}
|
|
2843
|
+
function getDependents(scenarioId) {
|
|
2844
|
+
const db2 = getDatabase();
|
|
2845
|
+
const rows = db2.query("SELECT scenario_id FROM scenario_dependencies WHERE depends_on = ?").all(scenarioId);
|
|
2846
|
+
return rows.map((r) => r.scenario_id);
|
|
2847
|
+
}
|
|
2848
|
+
function getTransitiveDependencies(scenarioId) {
|
|
2849
|
+
const db2 = getDatabase();
|
|
2850
|
+
const visited = new Set;
|
|
2851
|
+
const queue = [scenarioId];
|
|
2852
|
+
while (queue.length > 0) {
|
|
2853
|
+
const current = queue.shift();
|
|
2854
|
+
const deps = db2.query("SELECT depends_on FROM scenario_dependencies WHERE scenario_id = ?").all(current);
|
|
2855
|
+
for (const dep of deps) {
|
|
2856
|
+
if (!visited.has(dep.depends_on)) {
|
|
2857
|
+
visited.add(dep.depends_on);
|
|
2858
|
+
queue.push(dep.depends_on);
|
|
2859
|
+
}
|
|
2860
|
+
}
|
|
2861
|
+
}
|
|
2862
|
+
return Array.from(visited);
|
|
2863
|
+
}
|
|
2864
|
+
function topologicalSort(scenarioIds) {
|
|
2865
|
+
const db2 = getDatabase();
|
|
2866
|
+
const idSet = new Set(scenarioIds);
|
|
2867
|
+
const inDegree = new Map;
|
|
2868
|
+
const dependents = new Map;
|
|
2869
|
+
for (const id of scenarioIds) {
|
|
2870
|
+
inDegree.set(id, 0);
|
|
2871
|
+
dependents.set(id, []);
|
|
2872
|
+
}
|
|
2873
|
+
for (const id of scenarioIds) {
|
|
2874
|
+
const deps = db2.query("SELECT depends_on FROM scenario_dependencies WHERE scenario_id = ?").all(id);
|
|
2875
|
+
for (const dep of deps) {
|
|
2876
|
+
if (idSet.has(dep.depends_on)) {
|
|
2877
|
+
inDegree.set(id, (inDegree.get(id) ?? 0) + 1);
|
|
2878
|
+
dependents.get(dep.depends_on).push(id);
|
|
2879
|
+
}
|
|
2880
|
+
}
|
|
2881
|
+
}
|
|
2882
|
+
const queue = [];
|
|
2883
|
+
for (const [id, deg] of inDegree) {
|
|
2884
|
+
if (deg === 0)
|
|
2885
|
+
queue.push(id);
|
|
2886
|
+
}
|
|
2887
|
+
const sorted = [];
|
|
2888
|
+
while (queue.length > 0) {
|
|
2889
|
+
const current = queue.shift();
|
|
2890
|
+
sorted.push(current);
|
|
2891
|
+
for (const dep of dependents.get(current) ?? []) {
|
|
2892
|
+
const newDeg = (inDegree.get(dep) ?? 1) - 1;
|
|
2893
|
+
inDegree.set(dep, newDeg);
|
|
2894
|
+
if (newDeg === 0)
|
|
2895
|
+
queue.push(dep);
|
|
2896
|
+
}
|
|
2897
|
+
}
|
|
2898
|
+
if (sorted.length !== scenarioIds.length) {
|
|
2899
|
+
throw new DependencyCycleError("multiple", "multiple");
|
|
2900
|
+
}
|
|
2901
|
+
return sorted;
|
|
2902
|
+
}
|
|
2903
|
+
function createFlow(input) {
|
|
2904
|
+
const db2 = getDatabase();
|
|
2905
|
+
const id = uuid();
|
|
2906
|
+
const timestamp = now();
|
|
2907
|
+
db2.query(`
|
|
2908
|
+
INSERT INTO flows (id, project_id, name, description, scenario_ids, created_at, updated_at)
|
|
2909
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
2910
|
+
`).run(id, input.projectId ?? null, input.name, input.description ?? null, JSON.stringify(input.scenarioIds), timestamp, timestamp);
|
|
2911
|
+
return getFlow(id);
|
|
2912
|
+
}
|
|
2913
|
+
function getFlow(id) {
|
|
2914
|
+
const db2 = getDatabase();
|
|
2915
|
+
let row = db2.query("SELECT * FROM flows WHERE id = ?").get(id);
|
|
2916
|
+
if (row)
|
|
2917
|
+
return flowFromRow(row);
|
|
2918
|
+
const fullId = resolvePartialId("flows", id);
|
|
2919
|
+
if (fullId) {
|
|
2920
|
+
row = db2.query("SELECT * FROM flows WHERE id = ?").get(fullId);
|
|
2921
|
+
if (row)
|
|
2922
|
+
return flowFromRow(row);
|
|
2923
|
+
}
|
|
2924
|
+
return null;
|
|
2925
|
+
}
|
|
2926
|
+
function listFlows(projectId) {
|
|
2927
|
+
const db2 = getDatabase();
|
|
2928
|
+
if (projectId) {
|
|
2929
|
+
const rows2 = db2.query("SELECT * FROM flows WHERE project_id = ? ORDER BY created_at DESC").all(projectId);
|
|
2930
|
+
return rows2.map(flowFromRow);
|
|
2931
|
+
}
|
|
2932
|
+
const rows = db2.query("SELECT * FROM flows ORDER BY created_at DESC").all();
|
|
2933
|
+
return rows.map(flowFromRow);
|
|
2934
|
+
}
|
|
2935
|
+
function deleteFlow(id) {
|
|
2936
|
+
const db2 = getDatabase();
|
|
2937
|
+
const flow = getFlow(id);
|
|
2938
|
+
if (!flow)
|
|
2939
|
+
return false;
|
|
2940
|
+
const result = db2.query("DELETE FROM flows WHERE id = ?").run(flow.id);
|
|
2941
|
+
return result.changes > 0;
|
|
2942
|
+
}
|
|
2943
|
+
var init_flows = __esm(() => {
|
|
2944
|
+
init_database();
|
|
2945
|
+
init_database();
|
|
2946
|
+
init_types();
|
|
2947
|
+
});
|
|
2948
|
+
|
|
2949
|
+
// src/lib/openapi-import.ts
|
|
2950
|
+
var exports_openapi_import = {};
|
|
2951
|
+
__export(exports_openapi_import, {
|
|
2952
|
+
parseOpenAPISpec: () => parseOpenAPISpec,
|
|
2953
|
+
importFromOpenAPI: () => importFromOpenAPI
|
|
2954
|
+
});
|
|
2955
|
+
import { readFileSync as readFileSync5 } from "fs";
|
|
2956
|
+
function parseSpec(content) {
|
|
2957
|
+
try {
|
|
2958
|
+
return JSON.parse(content);
|
|
2959
|
+
} catch {
|
|
2960
|
+
throw new Error("Only JSON specs are supported. Convert YAML to JSON first: `cat spec.yaml | python -c 'import sys,yaml,json; json.dump(yaml.safe_load(sys.stdin),sys.stdout)' > spec.json`");
|
|
2961
|
+
}
|
|
2962
|
+
}
|
|
2963
|
+
function methodPriority(method) {
|
|
2964
|
+
switch (method.toUpperCase()) {
|
|
2965
|
+
case "GET":
|
|
2966
|
+
return "medium";
|
|
2967
|
+
case "POST":
|
|
2968
|
+
return "high";
|
|
2969
|
+
case "PUT":
|
|
2970
|
+
return "high";
|
|
2971
|
+
case "DELETE":
|
|
2972
|
+
return "critical";
|
|
2973
|
+
case "PATCH":
|
|
2974
|
+
return "medium";
|
|
2975
|
+
default:
|
|
2976
|
+
return "low";
|
|
2977
|
+
}
|
|
2978
|
+
}
|
|
2979
|
+
function parseOpenAPISpec(filePathOrUrl) {
|
|
2980
|
+
let content;
|
|
2981
|
+
if (filePathOrUrl.startsWith("http")) {
|
|
2982
|
+
throw new Error("URL fetching not supported yet. Download the spec file first.");
|
|
2983
|
+
}
|
|
2984
|
+
content = readFileSync5(filePathOrUrl, "utf-8");
|
|
2985
|
+
const spec = parseSpec(content);
|
|
2986
|
+
const isOpenAPI3 = !!spec.openapi;
|
|
2987
|
+
const isSwagger2 = !!spec.swagger;
|
|
2988
|
+
if (!isOpenAPI3 && !isSwagger2) {
|
|
2989
|
+
throw new Error("Not a valid OpenAPI 3.x or Swagger 2.0 spec");
|
|
2990
|
+
}
|
|
2991
|
+
const scenarios = [];
|
|
2992
|
+
const paths = spec.paths ?? {};
|
|
2993
|
+
for (const [path, methods] of Object.entries(paths)) {
|
|
2994
|
+
for (const [method, operation] of Object.entries(methods)) {
|
|
2995
|
+
if (["get", "post", "put", "delete", "patch"].indexOf(method.toLowerCase()) === -1)
|
|
2996
|
+
continue;
|
|
2997
|
+
const op = operation;
|
|
2998
|
+
const name = op.summary ?? op.operationId ?? `${method.toUpperCase()} ${path}`;
|
|
2999
|
+
const tags = op.tags ?? [];
|
|
3000
|
+
const requiresAuth = !!(op.security?.length ?? spec.security?.length);
|
|
3001
|
+
const steps = [];
|
|
3002
|
+
steps.push(`Navigate to the API endpoint: ${method.toUpperCase()} ${path}`);
|
|
3003
|
+
if (op.parameters?.length) {
|
|
3004
|
+
const required = op.parameters.filter((p) => p.required);
|
|
3005
|
+
if (required.length > 0) {
|
|
3006
|
+
steps.push(`Fill required parameters: ${required.map((p) => p.name).join(", ")}`);
|
|
3007
|
+
}
|
|
3008
|
+
}
|
|
3009
|
+
if (["post", "put", "patch"].includes(method.toLowerCase())) {
|
|
3010
|
+
steps.push("Fill the request body with valid test data");
|
|
3011
|
+
}
|
|
3012
|
+
steps.push("Submit the request");
|
|
3013
|
+
const responses = op.responses ?? {};
|
|
3014
|
+
const successCodes = Object.keys(responses).filter((c) => c.startsWith("2"));
|
|
3015
|
+
if (successCodes.length > 0) {
|
|
3016
|
+
steps.push(`Verify response status is ${successCodes.join(" or ")}`);
|
|
3017
|
+
} else {
|
|
3018
|
+
steps.push("Verify the response is successful");
|
|
3019
|
+
}
|
|
3020
|
+
const description = [
|
|
3021
|
+
op.description ?? `Test the ${method.toUpperCase()} ${path} endpoint.`,
|
|
3022
|
+
requiresAuth ? "This endpoint requires authentication." : ""
|
|
3023
|
+
].filter(Boolean).join(" ");
|
|
3024
|
+
scenarios.push({
|
|
3025
|
+
name,
|
|
3026
|
+
description,
|
|
3027
|
+
steps,
|
|
3028
|
+
tags: [...tags, "api", method.toLowerCase()],
|
|
3029
|
+
priority: methodPriority(method),
|
|
3030
|
+
targetPath: path,
|
|
3031
|
+
requiresAuth
|
|
3032
|
+
});
|
|
3033
|
+
}
|
|
3034
|
+
}
|
|
3035
|
+
return scenarios;
|
|
3036
|
+
}
|
|
3037
|
+
function importFromOpenAPI(filePathOrUrl, projectId) {
|
|
3038
|
+
const inputs = parseOpenAPISpec(filePathOrUrl);
|
|
3039
|
+
const scenarios = inputs.map((input) => createScenario({ ...input, projectId }));
|
|
3040
|
+
return { imported: scenarios.length, scenarios };
|
|
3041
|
+
}
|
|
3042
|
+
var init_openapi_import = __esm(() => {
|
|
3043
|
+
init_scenarios();
|
|
3044
|
+
});
|
|
3045
|
+
|
|
3046
|
+
// src/lib/recorder.ts
|
|
3047
|
+
var exports_recorder = {};
|
|
3048
|
+
__export(exports_recorder, {
|
|
3049
|
+
recordSession: () => recordSession,
|
|
3050
|
+
recordAndSave: () => recordAndSave,
|
|
3051
|
+
actionsToScenarioInput: () => actionsToScenarioInput
|
|
3052
|
+
});
|
|
3053
|
+
import { chromium as chromium2 } from "playwright";
|
|
3054
|
+
async function recordSession(url, options) {
|
|
3055
|
+
const browser = await chromium2.launch({ headless: false });
|
|
3056
|
+
const context = await browser.newContext({ viewport: { width: 1280, height: 720 } });
|
|
3057
|
+
const page = await context.newPage();
|
|
3058
|
+
const actions = [];
|
|
3059
|
+
const startTime = Date.now();
|
|
3060
|
+
const timeout = options?.timeout ?? 300000;
|
|
3061
|
+
page.on("framenavigated", (frame) => {
|
|
3062
|
+
if (frame === page.mainFrame()) {
|
|
3063
|
+
actions.push({ type: "navigate", url: frame.url(), timestamp: Date.now() - startTime });
|
|
3064
|
+
}
|
|
3065
|
+
});
|
|
3066
|
+
await page.addInitScript(() => {
|
|
3067
|
+
document.addEventListener("click", (e) => {
|
|
3068
|
+
const target = e.target;
|
|
3069
|
+
const selector = buildSelector(target);
|
|
3070
|
+
window.postMessage({ __testers_action: "click", selector }, "*");
|
|
3071
|
+
}, true);
|
|
3072
|
+
document.addEventListener("input", (e) => {
|
|
3073
|
+
const target = e.target;
|
|
3074
|
+
const selector = buildSelector(target);
|
|
3075
|
+
window.postMessage({ __testers_action: "fill", selector, value: target.value }, "*");
|
|
3076
|
+
}, true);
|
|
3077
|
+
document.addEventListener("change", (e) => {
|
|
3078
|
+
const target = e.target;
|
|
3079
|
+
if (target.tagName === "SELECT") {
|
|
3080
|
+
const selector = buildSelector(target);
|
|
3081
|
+
window.postMessage({ __testers_action: "select", selector, value: target.value }, "*");
|
|
3082
|
+
}
|
|
3083
|
+
}, true);
|
|
3084
|
+
document.addEventListener("keydown", (e) => {
|
|
3085
|
+
if (["Enter", "Tab", "Escape"].includes(e.key)) {
|
|
3086
|
+
window.postMessage({ __testers_action: "press", key: e.key }, "*");
|
|
3087
|
+
}
|
|
3088
|
+
}, true);
|
|
3089
|
+
function buildSelector(el) {
|
|
3090
|
+
if (el.id)
|
|
3091
|
+
return `#${el.id}`;
|
|
3092
|
+
if (el.getAttribute("data-testid"))
|
|
3093
|
+
return `[data-testid="${el.getAttribute("data-testid")}"]`;
|
|
3094
|
+
if (el.getAttribute("name"))
|
|
3095
|
+
return `${el.tagName.toLowerCase()}[name="${el.getAttribute("name")}"]`;
|
|
3096
|
+
if (el.getAttribute("aria-label"))
|
|
3097
|
+
return `[aria-label="${el.getAttribute("aria-label")}"]`;
|
|
3098
|
+
if (el.className && typeof el.className === "string") {
|
|
3099
|
+
const classes = el.className.trim().split(/\s+/).slice(0, 2).join(".");
|
|
3100
|
+
if (classes)
|
|
3101
|
+
return `${el.tagName.toLowerCase()}.${classes}`;
|
|
3102
|
+
}
|
|
3103
|
+
const text = el.textContent?.trim().slice(0, 30);
|
|
3104
|
+
if (text)
|
|
3105
|
+
return `text="${text}"`;
|
|
3106
|
+
return el.tagName.toLowerCase();
|
|
3107
|
+
}
|
|
3108
|
+
});
|
|
3109
|
+
const pollInterval = setInterval(async () => {
|
|
3110
|
+
try {
|
|
3111
|
+
const newActions = await page.evaluate(() => {
|
|
3112
|
+
const collected = window.__testers_collected ?? [];
|
|
3113
|
+
window.__testers_collected = [];
|
|
3114
|
+
return collected;
|
|
3115
|
+
});
|
|
3116
|
+
for (const a of newActions) {
|
|
3117
|
+
actions.push({
|
|
3118
|
+
type: a["type"],
|
|
3119
|
+
selector: a["selector"],
|
|
3120
|
+
value: a["value"],
|
|
3121
|
+
key: a["key"],
|
|
3122
|
+
timestamp: Date.now() - startTime
|
|
3123
|
+
});
|
|
3124
|
+
}
|
|
3125
|
+
} catch {}
|
|
3126
|
+
}, 500);
|
|
3127
|
+
await page.exposeFunction("__testersRecord", (action) => {
|
|
3128
|
+
actions.push({ ...action, timestamp: Date.now() - startTime });
|
|
3129
|
+
});
|
|
3130
|
+
await page.addInitScript(() => {
|
|
3131
|
+
window.addEventListener("message", (e) => {
|
|
3132
|
+
if (e.data?.__testers_action) {
|
|
3133
|
+
const { __testers_action, ...rest } = e.data;
|
|
3134
|
+
window.__testersRecord({ type: __testers_action, ...rest });
|
|
3135
|
+
}
|
|
3136
|
+
});
|
|
3137
|
+
});
|
|
3138
|
+
await page.goto(url);
|
|
3139
|
+
actions.push({ type: "navigate", url, timestamp: 0 });
|
|
3140
|
+
console.log(`
|
|
3141
|
+
Recording started. Interact with the browser.`);
|
|
3142
|
+
console.log(` Close the browser window or wait ${timeout / 1000}s to stop.
|
|
3143
|
+
`);
|
|
3144
|
+
await Promise.race([
|
|
3145
|
+
page.waitForEvent("close").catch(() => {}),
|
|
3146
|
+
context.waitForEvent("close").catch(() => {}),
|
|
3147
|
+
new Promise((resolve) => setTimeout(resolve, timeout))
|
|
3148
|
+
]);
|
|
3149
|
+
clearInterval(pollInterval);
|
|
3150
|
+
try {
|
|
3151
|
+
await browser.close();
|
|
3152
|
+
} catch {}
|
|
3153
|
+
return {
|
|
3154
|
+
actions,
|
|
3155
|
+
url,
|
|
3156
|
+
duration: Date.now() - startTime
|
|
3157
|
+
};
|
|
3158
|
+
}
|
|
3159
|
+
function actionsToScenarioInput(recording, name, projectId) {
|
|
3160
|
+
const steps = [];
|
|
3161
|
+
const seenFills = new Map;
|
|
3162
|
+
for (const action of recording.actions) {
|
|
3163
|
+
switch (action.type) {
|
|
3164
|
+
case "navigate":
|
|
3165
|
+
if (action.url)
|
|
3166
|
+
steps.push(`Navigate to ${action.url}`);
|
|
3167
|
+
break;
|
|
3168
|
+
case "click":
|
|
3169
|
+
if (action.selector)
|
|
3170
|
+
steps.push(`Click ${action.selector}`);
|
|
3171
|
+
break;
|
|
3172
|
+
case "fill":
|
|
3173
|
+
if (action.selector && action.value) {
|
|
3174
|
+
seenFills.set(action.selector, action.value);
|
|
3175
|
+
}
|
|
3176
|
+
break;
|
|
3177
|
+
case "select":
|
|
3178
|
+
if (action.selector && action.value)
|
|
3179
|
+
steps.push(`Select "${action.value}" in ${action.selector}`);
|
|
3180
|
+
break;
|
|
3181
|
+
case "press":
|
|
3182
|
+
if (action.key)
|
|
3183
|
+
steps.push(`Press ${action.key}`);
|
|
3184
|
+
break;
|
|
3185
|
+
}
|
|
3186
|
+
}
|
|
3187
|
+
for (const [selector, value] of seenFills) {
|
|
3188
|
+
steps.push(`Fill ${selector} with "${value}"`);
|
|
3189
|
+
}
|
|
3190
|
+
return {
|
|
3191
|
+
name,
|
|
3192
|
+
description: `Recorded session on ${recording.url} (${(recording.duration / 1000).toFixed(0)}s, ${recording.actions.length} actions)`,
|
|
3193
|
+
steps,
|
|
3194
|
+
tags: ["recorded"],
|
|
3195
|
+
projectId
|
|
3196
|
+
};
|
|
3197
|
+
}
|
|
3198
|
+
async function recordAndSave(url, name, projectId) {
|
|
3199
|
+
const recording = await recordSession(url);
|
|
3200
|
+
const input = actionsToScenarioInput(recording, name, projectId);
|
|
3201
|
+
const scenario = createScenario(input);
|
|
3202
|
+
return { recording, scenario };
|
|
3203
|
+
}
|
|
3204
|
+
var init_recorder = __esm(() => {
|
|
3205
|
+
init_scenarios();
|
|
3206
|
+
});
|
|
3207
|
+
|
|
3208
|
+
// node_modules/commander/esm.mjs
|
|
3209
|
+
var import__ = __toESM(require_commander(), 1);
|
|
3210
|
+
var {
|
|
3211
|
+
program,
|
|
3212
|
+
createCommand,
|
|
3213
|
+
createArgument,
|
|
3214
|
+
createOption,
|
|
3215
|
+
CommanderError,
|
|
3216
|
+
InvalidArgumentError,
|
|
3217
|
+
InvalidOptionArgumentError,
|
|
3218
|
+
Command,
|
|
3219
|
+
Argument,
|
|
3220
|
+
Option,
|
|
3221
|
+
Help
|
|
3222
|
+
} = import__.default;
|
|
2749
3223
|
|
|
2750
3224
|
// src/cli/index.tsx
|
|
3225
|
+
init_scenarios();
|
|
2751
3226
|
init_runs();
|
|
3227
|
+
import chalk5 from "chalk";
|
|
3228
|
+
import { readFileSync as readFileSync6, readdirSync, writeFileSync as writeFileSync3 } from "fs";
|
|
3229
|
+
import { join as join6, resolve } from "path";
|
|
2752
3230
|
|
|
2753
3231
|
// src/db/results.ts
|
|
2754
3232
|
init_types();
|
|
@@ -2854,6 +3332,7 @@ function listScreenshots(resultId) {
|
|
|
2854
3332
|
|
|
2855
3333
|
// src/lib/runner.ts
|
|
2856
3334
|
init_runs();
|
|
3335
|
+
init_scenarios();
|
|
2857
3336
|
|
|
2858
3337
|
// src/lib/browser.ts
|
|
2859
3338
|
init_types();
|
|
@@ -3832,7 +4311,105 @@ function loadConfig() {
|
|
|
3832
4311
|
if (envApiKey) {
|
|
3833
4312
|
config.anthropicApiKey = envApiKey;
|
|
3834
4313
|
}
|
|
3835
|
-
return config;
|
|
4314
|
+
return config;
|
|
4315
|
+
}
|
|
4316
|
+
|
|
4317
|
+
// src/lib/webhooks.ts
|
|
4318
|
+
init_database();
|
|
4319
|
+
function fromRow(row) {
|
|
4320
|
+
return {
|
|
4321
|
+
id: row.id,
|
|
4322
|
+
url: row.url,
|
|
4323
|
+
events: JSON.parse(row.events),
|
|
4324
|
+
projectId: row.project_id,
|
|
4325
|
+
secret: row.secret,
|
|
4326
|
+
active: row.active === 1,
|
|
4327
|
+
createdAt: row.created_at
|
|
4328
|
+
};
|
|
4329
|
+
}
|
|
4330
|
+
function listWebhooks(projectId) {
|
|
4331
|
+
const db2 = getDatabase();
|
|
4332
|
+
let query = "SELECT * FROM webhooks WHERE active = 1";
|
|
4333
|
+
const params = [];
|
|
4334
|
+
if (projectId) {
|
|
4335
|
+
query += " AND (project_id = ? OR project_id IS NULL)";
|
|
4336
|
+
params.push(projectId);
|
|
4337
|
+
}
|
|
4338
|
+
query += " ORDER BY created_at DESC";
|
|
4339
|
+
const rows = db2.query(query).all(...params);
|
|
4340
|
+
return rows.map(fromRow);
|
|
4341
|
+
}
|
|
4342
|
+
function signPayload(body, secret) {
|
|
4343
|
+
const encoder = new TextEncoder;
|
|
4344
|
+
const key = encoder.encode(secret);
|
|
4345
|
+
const data = encoder.encode(body);
|
|
4346
|
+
let hash = 0;
|
|
4347
|
+
for (let i = 0;i < data.length; i++) {
|
|
4348
|
+
hash = (hash << 5) - hash + data[i] + (key[i % key.length] ?? 0) | 0;
|
|
4349
|
+
}
|
|
4350
|
+
return `sha256=${Math.abs(hash).toString(16).padStart(16, "0")}`;
|
|
4351
|
+
}
|
|
4352
|
+
function formatSlackPayload(payload) {
|
|
4353
|
+
const status = payload.run.status === "passed" ? ":white_check_mark:" : ":x:";
|
|
4354
|
+
const color = payload.run.status === "passed" ? "#22c55e" : "#ef4444";
|
|
4355
|
+
return {
|
|
4356
|
+
attachments: [
|
|
4357
|
+
{
|
|
4358
|
+
color,
|
|
4359
|
+
blocks: [
|
|
4360
|
+
{
|
|
4361
|
+
type: "section",
|
|
4362
|
+
text: {
|
|
4363
|
+
type: "mrkdwn",
|
|
4364
|
+
text: `${status} *Test Run ${payload.run.status.toUpperCase()}*
|
|
4365
|
+
` + `URL: ${payload.run.url}
|
|
4366
|
+
` + `Results: ${payload.run.passed}/${payload.run.total} passed` + (payload.run.failed > 0 ? ` (${payload.run.failed} failed)` : "") + (payload.schedule ? `
|
|
4367
|
+
Schedule: ${payload.schedule.name}` : "")
|
|
4368
|
+
}
|
|
4369
|
+
}
|
|
4370
|
+
]
|
|
4371
|
+
}
|
|
4372
|
+
]
|
|
4373
|
+
};
|
|
4374
|
+
}
|
|
4375
|
+
async function dispatchWebhooks(event, run, schedule) {
|
|
4376
|
+
const webhooks = listWebhooks(run.projectId ?? undefined);
|
|
4377
|
+
const payload = {
|
|
4378
|
+
event,
|
|
4379
|
+
run: {
|
|
4380
|
+
id: run.id,
|
|
4381
|
+
url: run.url,
|
|
4382
|
+
status: run.status,
|
|
4383
|
+
passed: run.passed,
|
|
4384
|
+
failed: run.failed,
|
|
4385
|
+
total: run.total
|
|
4386
|
+
},
|
|
4387
|
+
schedule,
|
|
4388
|
+
timestamp: new Date().toISOString()
|
|
4389
|
+
};
|
|
4390
|
+
for (const webhook of webhooks) {
|
|
4391
|
+
if (!webhook.events.includes(event) && !webhook.events.includes("*"))
|
|
4392
|
+
continue;
|
|
4393
|
+
const isSlack = webhook.url.includes("hooks.slack.com");
|
|
4394
|
+
const body = isSlack ? JSON.stringify(formatSlackPayload(payload)) : JSON.stringify(payload);
|
|
4395
|
+
const headers = {
|
|
4396
|
+
"Content-Type": "application/json"
|
|
4397
|
+
};
|
|
4398
|
+
if (webhook.secret) {
|
|
4399
|
+
headers["X-Testers-Signature"] = signPayload(body, webhook.secret);
|
|
4400
|
+
}
|
|
4401
|
+
try {
|
|
4402
|
+
const response = await fetch(webhook.url, {
|
|
4403
|
+
method: "POST",
|
|
4404
|
+
headers,
|
|
4405
|
+
body
|
|
4406
|
+
});
|
|
4407
|
+
if (!response.ok) {
|
|
4408
|
+
await new Promise((r) => setTimeout(r, 5000));
|
|
4409
|
+
await fetch(webhook.url, { method: "POST", headers, body });
|
|
4410
|
+
}
|
|
4411
|
+
} catch {}
|
|
4412
|
+
}
|
|
3836
4413
|
}
|
|
3837
4414
|
|
|
3838
4415
|
// src/lib/runner.ts
|
|
@@ -3844,6 +4421,20 @@ function emit(event) {
|
|
|
3844
4421
|
if (eventHandler)
|
|
3845
4422
|
eventHandler(event);
|
|
3846
4423
|
}
|
|
4424
|
+
function withTimeout(promise, ms, label) {
|
|
4425
|
+
return new Promise((resolve, reject) => {
|
|
4426
|
+
const timer = setTimeout(() => {
|
|
4427
|
+
reject(new Error(`Scenario timeout after ${ms}ms: ${label}`));
|
|
4428
|
+
}, ms);
|
|
4429
|
+
promise.then((val) => {
|
|
4430
|
+
clearTimeout(timer);
|
|
4431
|
+
resolve(val);
|
|
4432
|
+
}, (err) => {
|
|
4433
|
+
clearTimeout(timer);
|
|
4434
|
+
reject(err);
|
|
4435
|
+
});
|
|
4436
|
+
});
|
|
4437
|
+
}
|
|
3847
4438
|
async function runSingleScenario(scenario, runId, options) {
|
|
3848
4439
|
const config = loadConfig();
|
|
3849
4440
|
const model = resolveModel(options.model ?? scenario.model ?? config.defaultModel);
|
|
@@ -3866,8 +4457,9 @@ async function runSingleScenario(scenario, runId, options) {
|
|
|
3866
4457
|
viewport: config.browser.viewport
|
|
3867
4458
|
});
|
|
3868
4459
|
const targetUrl = scenario.targetPath ? `${options.url.replace(/\/$/, "")}${scenario.targetPath}` : options.url;
|
|
3869
|
-
|
|
3870
|
-
|
|
4460
|
+
const scenarioTimeout = scenario.timeoutMs ?? options.timeout ?? config.browser.timeout ?? 60000;
|
|
4461
|
+
await page.goto(targetUrl, { timeout: Math.min(scenarioTimeout, 30000) });
|
|
4462
|
+
const agentResult = await withTimeout(runAgentLoop({
|
|
3871
4463
|
client,
|
|
3872
4464
|
page,
|
|
3873
4465
|
scenario,
|
|
@@ -3888,7 +4480,7 @@ async function runSingleScenario(scenario, runId, options) {
|
|
|
3888
4480
|
stepNumber: stepEvent.stepNumber
|
|
3889
4481
|
});
|
|
3890
4482
|
}
|
|
3891
|
-
});
|
|
4483
|
+
}), scenarioTimeout, scenario.name);
|
|
3892
4484
|
for (const ss of agentResult.screenshots) {
|
|
3893
4485
|
createScreenshot({
|
|
3894
4486
|
resultId: result.id,
|
|
@@ -3940,24 +4532,70 @@ async function runBatch(scenarios, options) {
|
|
|
3940
4532
|
projectId: options.projectId
|
|
3941
4533
|
});
|
|
3942
4534
|
updateRun(run.id, { status: "running", total: scenarios.length });
|
|
4535
|
+
let sortedScenarios = scenarios;
|
|
4536
|
+
try {
|
|
4537
|
+
const { topologicalSort: topologicalSort2 } = await Promise.resolve().then(() => (init_flows(), exports_flows));
|
|
4538
|
+
const scenarioIds = scenarios.map((s) => s.id);
|
|
4539
|
+
const sortedIds = topologicalSort2(scenarioIds);
|
|
4540
|
+
const scenarioMap = new Map(scenarios.map((s) => [s.id, s]));
|
|
4541
|
+
sortedScenarios = sortedIds.map((id) => scenarioMap.get(id)).filter((s) => s !== undefined);
|
|
4542
|
+
for (const s of scenarios) {
|
|
4543
|
+
if (!sortedIds.includes(s.id))
|
|
4544
|
+
sortedScenarios.push(s);
|
|
4545
|
+
}
|
|
4546
|
+
} catch {}
|
|
3943
4547
|
const results = [];
|
|
4548
|
+
const failedScenarioIds = new Set;
|
|
4549
|
+
const canRun = async (scenario) => {
|
|
4550
|
+
try {
|
|
4551
|
+
const { getDependencies: getDependencies2 } = await Promise.resolve().then(() => (init_flows(), exports_flows));
|
|
4552
|
+
const deps = getDependencies2(scenario.id);
|
|
4553
|
+
for (const depId of deps) {
|
|
4554
|
+
if (failedScenarioIds.has(depId))
|
|
4555
|
+
return false;
|
|
4556
|
+
}
|
|
4557
|
+
} catch {}
|
|
4558
|
+
return true;
|
|
4559
|
+
};
|
|
3944
4560
|
if (parallel <= 1) {
|
|
3945
|
-
for (const scenario of
|
|
4561
|
+
for (const scenario of sortedScenarios) {
|
|
4562
|
+
if (!await canRun(scenario)) {
|
|
4563
|
+
const result2 = createResult({ runId: run.id, scenarioId: scenario.id, model, stepsTotal: 0 });
|
|
4564
|
+
const skipped = updateResult(result2.id, { status: "skipped", error: "Skipped: dependency failed" });
|
|
4565
|
+
results.push(skipped);
|
|
4566
|
+
failedScenarioIds.add(scenario.id);
|
|
4567
|
+
emit({ type: "scenario:error", scenarioId: scenario.id, scenarioName: scenario.name, error: "Dependency failed \u2014 skipped", runId: run.id });
|
|
4568
|
+
continue;
|
|
4569
|
+
}
|
|
3946
4570
|
const result = await runSingleScenario(scenario, run.id, options);
|
|
3947
4571
|
results.push(result);
|
|
4572
|
+
if (result.status === "failed" || result.status === "error") {
|
|
4573
|
+
failedScenarioIds.add(scenario.id);
|
|
4574
|
+
}
|
|
3948
4575
|
}
|
|
3949
4576
|
} else {
|
|
3950
|
-
const queue = [...
|
|
4577
|
+
const queue = [...sortedScenarios];
|
|
3951
4578
|
const running = [];
|
|
3952
4579
|
const processNext = async () => {
|
|
3953
4580
|
const scenario = queue.shift();
|
|
3954
4581
|
if (!scenario)
|
|
3955
4582
|
return;
|
|
4583
|
+
if (!await canRun(scenario)) {
|
|
4584
|
+
const result2 = createResult({ runId: run.id, scenarioId: scenario.id, model, stepsTotal: 0 });
|
|
4585
|
+
const skipped = updateResult(result2.id, { status: "skipped", error: "Skipped: dependency failed" });
|
|
4586
|
+
results.push(skipped);
|
|
4587
|
+
failedScenarioIds.add(scenario.id);
|
|
4588
|
+
await processNext();
|
|
4589
|
+
return;
|
|
4590
|
+
}
|
|
3956
4591
|
const result = await runSingleScenario(scenario, run.id, options);
|
|
3957
4592
|
results.push(result);
|
|
4593
|
+
if (result.status === "failed" || result.status === "error") {
|
|
4594
|
+
failedScenarioIds.add(scenario.id);
|
|
4595
|
+
}
|
|
3958
4596
|
await processNext();
|
|
3959
4597
|
};
|
|
3960
|
-
const workers = Math.min(parallel,
|
|
4598
|
+
const workers = Math.min(parallel, sortedScenarios.length);
|
|
3961
4599
|
for (let i = 0;i < workers; i++) {
|
|
3962
4600
|
running.push(processNext());
|
|
3963
4601
|
}
|
|
@@ -3974,6 +4612,8 @@ async function runBatch(scenarios, options) {
|
|
|
3974
4612
|
finished_at: new Date().toISOString()
|
|
3975
4613
|
});
|
|
3976
4614
|
emit({ type: "run:complete", runId: run.id });
|
|
4615
|
+
const eventType = finalRun.status === "failed" ? "failed" : "completed";
|
|
4616
|
+
dispatchWebhooks(eventType, finalRun).catch(() => {});
|
|
3977
4617
|
return { run: finalRun, results };
|
|
3978
4618
|
}
|
|
3979
4619
|
async function runByFilter(options) {
|
|
@@ -4059,6 +4699,9 @@ function startRunAsync(options) {
|
|
|
4059
4699
|
finished_at: new Date().toISOString()
|
|
4060
4700
|
});
|
|
4061
4701
|
emit({ type: "run:complete", runId: run.id });
|
|
4702
|
+
const asyncRun = getRun(run.id);
|
|
4703
|
+
if (asyncRun)
|
|
4704
|
+
dispatchWebhooks(asyncRun.status === "failed" ? "failed" : "completed", asyncRun).catch(() => {});
|
|
4062
4705
|
} catch (error) {
|
|
4063
4706
|
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
4064
4707
|
updateRun(run.id, {
|
|
@@ -4066,6 +4709,9 @@ function startRunAsync(options) {
|
|
|
4066
4709
|
finished_at: new Date().toISOString()
|
|
4067
4710
|
});
|
|
4068
4711
|
emit({ type: "run:complete", runId: run.id, error: errorMsg });
|
|
4712
|
+
const failedRun = getRun(run.id);
|
|
4713
|
+
if (failedRun)
|
|
4714
|
+
dispatchWebhooks("failed", failedRun).catch(() => {});
|
|
4069
4715
|
}
|
|
4070
4716
|
})();
|
|
4071
4717
|
return { runId: run.id, scenarioCount: scenarios.length };
|
|
@@ -4082,6 +4728,7 @@ function estimateCost(model, tokens) {
|
|
|
4082
4728
|
|
|
4083
4729
|
// src/lib/reporter.ts
|
|
4084
4730
|
import chalk from "chalk";
|
|
4731
|
+
init_scenarios();
|
|
4085
4732
|
function formatTerminal(run, results) {
|
|
4086
4733
|
const lines = [];
|
|
4087
4734
|
lines.push("");
|
|
@@ -4235,11 +4882,12 @@ function formatScenarioList(scenarios) {
|
|
|
4235
4882
|
}
|
|
4236
4883
|
|
|
4237
4884
|
// src/lib/todos-connector.ts
|
|
4885
|
+
init_scenarios();
|
|
4886
|
+
init_types();
|
|
4238
4887
|
import { Database as Database2 } from "bun:sqlite";
|
|
4239
4888
|
import { existsSync as existsSync4 } from "fs";
|
|
4240
4889
|
import { join as join4 } from "path";
|
|
4241
4890
|
import { homedir as homedir4 } from "os";
|
|
4242
|
-
init_types();
|
|
4243
4891
|
function resolveTodosDbPath() {
|
|
4244
4892
|
const envPath = process.env["TODOS_DB_PATH"];
|
|
4245
4893
|
if (envPath)
|
|
@@ -4337,6 +4985,7 @@ function importFromTodos(options = {}) {
|
|
|
4337
4985
|
}
|
|
4338
4986
|
|
|
4339
4987
|
// src/lib/init.ts
|
|
4988
|
+
init_scenarios();
|
|
4340
4989
|
import { existsSync as existsSync5, readFileSync as readFileSync2, writeFileSync as writeFileSync2, mkdirSync as mkdirSync3 } from "fs";
|
|
4341
4990
|
import { join as join5, basename } from "path";
|
|
4342
4991
|
import { homedir as homedir5 } from "os";
|
|
@@ -4494,6 +5143,7 @@ function initProject(options) {
|
|
|
4494
5143
|
}
|
|
4495
5144
|
|
|
4496
5145
|
// src/lib/smoke.ts
|
|
5146
|
+
init_scenarios();
|
|
4497
5147
|
init_runs();
|
|
4498
5148
|
var SMOKE_DESCRIPTION = `You are performing an autonomous smoke test of this web application. Your job is to explore as many pages as possible and find issues. Follow these instructions:
|
|
4499
5149
|
|
|
@@ -4715,6 +5365,7 @@ function formatSmokeReport(result) {
|
|
|
4715
5365
|
// src/lib/diff.ts
|
|
4716
5366
|
init_runs();
|
|
4717
5367
|
import chalk2 from "chalk";
|
|
5368
|
+
init_scenarios();
|
|
4718
5369
|
function diffRuns(runId1, runId2) {
|
|
4719
5370
|
const run1 = getRun(runId1);
|
|
4720
5371
|
if (!run1) {
|
|
@@ -4860,14 +5511,145 @@ function formatDiffJSON(diff) {
|
|
|
4860
5511
|
return JSON.stringify(diff, null, 2);
|
|
4861
5512
|
}
|
|
4862
5513
|
|
|
5514
|
+
// src/lib/visual-diff.ts
|
|
5515
|
+
import { readFileSync as readFileSync3, existsSync as existsSync6 } from "fs";
|
|
5516
|
+
import chalk3 from "chalk";
|
|
5517
|
+
init_runs();
|
|
5518
|
+
init_scenarios();
|
|
5519
|
+
init_database();
|
|
5520
|
+
var DEFAULT_THRESHOLD = 0.1;
|
|
5521
|
+
function setBaseline(runId) {
|
|
5522
|
+
const run = getRun(runId);
|
|
5523
|
+
if (!run) {
|
|
5524
|
+
throw new Error(`Run not found: ${runId}`);
|
|
5525
|
+
}
|
|
5526
|
+
const db2 = getDatabase();
|
|
5527
|
+
if (run.projectId) {
|
|
5528
|
+
db2.query("UPDATE runs SET is_baseline = 0 WHERE project_id = ? AND is_baseline = 1").run(run.projectId);
|
|
5529
|
+
} else {
|
|
5530
|
+
db2.query("UPDATE runs SET is_baseline = 0 WHERE project_id IS NULL AND is_baseline = 1").run();
|
|
5531
|
+
}
|
|
5532
|
+
updateRun(run.id, { is_baseline: 1 });
|
|
5533
|
+
}
|
|
5534
|
+
function compareImages(image1Path, image2Path) {
|
|
5535
|
+
if (!existsSync6(image1Path)) {
|
|
5536
|
+
throw new Error(`Baseline image not found: ${image1Path}`);
|
|
5537
|
+
}
|
|
5538
|
+
if (!existsSync6(image2Path)) {
|
|
5539
|
+
throw new Error(`Current image not found: ${image2Path}`);
|
|
5540
|
+
}
|
|
5541
|
+
const buf1 = readFileSync3(image1Path);
|
|
5542
|
+
const buf2 = readFileSync3(image2Path);
|
|
5543
|
+
if (buf1.equals(buf2)) {
|
|
5544
|
+
const estimatedPixels = Math.max(1, Math.floor(buf1.length / 4));
|
|
5545
|
+
return { diffPercent: 0, diffPixels: 0, totalPixels: estimatedPixels };
|
|
5546
|
+
}
|
|
5547
|
+
if (buf1.length !== buf2.length) {
|
|
5548
|
+
const maxLen = Math.max(buf1.length, buf2.length);
|
|
5549
|
+
const estimatedPixels = Math.max(1, Math.floor(maxLen / 4));
|
|
5550
|
+
return { diffPercent: 100, diffPixels: estimatedPixels, totalPixels: estimatedPixels };
|
|
5551
|
+
}
|
|
5552
|
+
let diffBytes = 0;
|
|
5553
|
+
for (let i = 0;i < buf1.length; i++) {
|
|
5554
|
+
if (buf1[i] !== buf2[i]) {
|
|
5555
|
+
diffBytes++;
|
|
5556
|
+
}
|
|
5557
|
+
}
|
|
5558
|
+
const totalPixels = Math.max(1, Math.floor(buf1.length / 4));
|
|
5559
|
+
const diffPixels = Math.max(1, Math.floor(diffBytes / 4));
|
|
5560
|
+
const diffPercent = parseFloat((diffBytes / buf1.length * 100).toFixed(4));
|
|
5561
|
+
return { diffPercent, diffPixels, totalPixels };
|
|
5562
|
+
}
|
|
5563
|
+
function compareRunScreenshots(runId, baselineRunId, threshold = DEFAULT_THRESHOLD) {
|
|
5564
|
+
const run = getRun(runId);
|
|
5565
|
+
if (!run)
|
|
5566
|
+
throw new Error(`Run not found: ${runId}`);
|
|
5567
|
+
const baselineRun = getRun(baselineRunId);
|
|
5568
|
+
if (!baselineRun)
|
|
5569
|
+
throw new Error(`Baseline run not found: ${baselineRunId}`);
|
|
5570
|
+
const currentResults = getResultsByRun(run.id);
|
|
5571
|
+
const baselineResults = getResultsByRun(baselineRun.id);
|
|
5572
|
+
const baselineMap = new Map;
|
|
5573
|
+
for (const result of baselineResults) {
|
|
5574
|
+
const screenshots = listScreenshots(result.id);
|
|
5575
|
+
for (const ss of screenshots) {
|
|
5576
|
+
const key = `${result.scenarioId}:${ss.stepNumber}`;
|
|
5577
|
+
baselineMap.set(key, { path: ss.filePath, action: ss.action });
|
|
5578
|
+
}
|
|
5579
|
+
}
|
|
5580
|
+
const results = [];
|
|
5581
|
+
for (const result of currentResults) {
|
|
5582
|
+
const screenshots = listScreenshots(result.id);
|
|
5583
|
+
for (const ss of screenshots) {
|
|
5584
|
+
const key = `${result.scenarioId}:${ss.stepNumber}`;
|
|
5585
|
+
const baseline = baselineMap.get(key);
|
|
5586
|
+
if (!baseline)
|
|
5587
|
+
continue;
|
|
5588
|
+
if (!existsSync6(baseline.path) || !existsSync6(ss.filePath))
|
|
5589
|
+
continue;
|
|
5590
|
+
try {
|
|
5591
|
+
const comparison = compareImages(baseline.path, ss.filePath);
|
|
5592
|
+
results.push({
|
|
5593
|
+
scenarioId: result.scenarioId,
|
|
5594
|
+
stepNumber: ss.stepNumber,
|
|
5595
|
+
action: ss.action,
|
|
5596
|
+
baselinePath: baseline.path,
|
|
5597
|
+
currentPath: ss.filePath,
|
|
5598
|
+
diffPercent: comparison.diffPercent,
|
|
5599
|
+
isRegression: comparison.diffPercent > threshold
|
|
5600
|
+
});
|
|
5601
|
+
} catch {}
|
|
5602
|
+
}
|
|
5603
|
+
}
|
|
5604
|
+
return results;
|
|
5605
|
+
}
|
|
5606
|
+
function formatVisualDiffTerminal(results, threshold = DEFAULT_THRESHOLD) {
|
|
5607
|
+
if (results.length === 0) {
|
|
5608
|
+
return chalk3.dim(`
|
|
5609
|
+
No screenshot comparisons found.
|
|
5610
|
+
`);
|
|
5611
|
+
}
|
|
5612
|
+
const lines = [];
|
|
5613
|
+
lines.push("");
|
|
5614
|
+
lines.push(chalk3.bold(" Visual Regression Summary"));
|
|
5615
|
+
lines.push("");
|
|
5616
|
+
const regressions = results.filter((r) => r.diffPercent >= threshold);
|
|
5617
|
+
const passed = results.filter((r) => r.diffPercent < threshold);
|
|
5618
|
+
if (regressions.length > 0) {
|
|
5619
|
+
lines.push(chalk3.red.bold(` Regressions (${regressions.length}):`));
|
|
5620
|
+
for (const r of regressions) {
|
|
5621
|
+
const scenario = getScenario(r.scenarioId);
|
|
5622
|
+
const label = scenario ? `${scenario.shortId}: ${scenario.name}` : r.scenarioId.slice(0, 8);
|
|
5623
|
+
const pct = chalk3.red(`${r.diffPercent.toFixed(2)}%`);
|
|
5624
|
+
lines.push(` ${chalk3.red("!")} ${label} step ${r.stepNumber} (${r.action}) \u2014 ${pct} diff`);
|
|
5625
|
+
}
|
|
5626
|
+
lines.push("");
|
|
5627
|
+
}
|
|
5628
|
+
if (passed.length > 0) {
|
|
5629
|
+
lines.push(chalk3.green.bold(` Passed (${passed.length}):`));
|
|
5630
|
+
for (const r of passed) {
|
|
5631
|
+
const scenario = getScenario(r.scenarioId);
|
|
5632
|
+
const label = scenario ? `${scenario.shortId}: ${scenario.name}` : r.scenarioId.slice(0, 8);
|
|
5633
|
+
const pct = chalk3.green(`${r.diffPercent.toFixed(2)}%`);
|
|
5634
|
+
lines.push(` ${chalk3.green("\u2713")} ${label} step ${r.stepNumber} (${r.action}) \u2014 ${pct} diff`);
|
|
5635
|
+
}
|
|
5636
|
+
lines.push("");
|
|
5637
|
+
}
|
|
5638
|
+
lines.push(chalk3.bold(` Visual Summary: ${regressions.length} regressions, ${passed.length} passed (threshold: ${threshold}%)`));
|
|
5639
|
+
lines.push("");
|
|
5640
|
+
return lines.join(`
|
|
5641
|
+
`);
|
|
5642
|
+
}
|
|
5643
|
+
|
|
4863
5644
|
// src/lib/report.ts
|
|
4864
5645
|
init_runs();
|
|
4865
|
-
import { readFileSync as
|
|
5646
|
+
import { readFileSync as readFileSync4, existsSync as existsSync7 } from "fs";
|
|
5647
|
+
init_scenarios();
|
|
4866
5648
|
function imageToBase64(filePath) {
|
|
4867
|
-
if (!filePath || !
|
|
5649
|
+
if (!filePath || !existsSync7(filePath))
|
|
4868
5650
|
return "";
|
|
4869
5651
|
try {
|
|
4870
|
-
const buffer =
|
|
5652
|
+
const buffer = readFileSync4(filePath);
|
|
4871
5653
|
const base64 = buffer.toString("base64");
|
|
4872
5654
|
return `data:image/png;base64,${base64}`;
|
|
4873
5655
|
} catch {
|
|
@@ -5061,7 +5843,7 @@ function generateLatestReport() {
|
|
|
5061
5843
|
|
|
5062
5844
|
// src/lib/costs.ts
|
|
5063
5845
|
init_database();
|
|
5064
|
-
import
|
|
5846
|
+
import chalk4 from "chalk";
|
|
5065
5847
|
function getDateFilter(period) {
|
|
5066
5848
|
switch (period) {
|
|
5067
5849
|
case "day":
|
|
@@ -5166,15 +5948,15 @@ function formatTokens(tokens) {
|
|
|
5166
5948
|
function formatCostsTerminal(summary) {
|
|
5167
5949
|
const lines = [];
|
|
5168
5950
|
lines.push("");
|
|
5169
|
-
lines.push(
|
|
5951
|
+
lines.push(chalk4.bold(` Cost Summary (${summary.period})`));
|
|
5170
5952
|
lines.push("");
|
|
5171
|
-
lines.push(` Total: ${
|
|
5172
|
-
lines.push(` Avg/run: ${
|
|
5173
|
-
lines.push(` Est/month: ${
|
|
5953
|
+
lines.push(` Total: ${chalk4.yellow(formatDollars(summary.totalCostCents))} (${formatTokens(summary.totalTokens)} tokens across ${summary.runCount} runs)`);
|
|
5954
|
+
lines.push(` Avg/run: ${chalk4.yellow(formatDollars(summary.avgCostPerRun))}`);
|
|
5955
|
+
lines.push(` Est/month: ${chalk4.yellow(formatDollars(summary.estimatedMonthlyCents))}`);
|
|
5174
5956
|
const modelEntries = Object.entries(summary.byModel);
|
|
5175
5957
|
if (modelEntries.length > 0) {
|
|
5176
5958
|
lines.push("");
|
|
5177
|
-
lines.push(
|
|
5959
|
+
lines.push(chalk4.bold(" By Model"));
|
|
5178
5960
|
lines.push(` ${"Model".padEnd(40)} ${"Cost".padEnd(12)} ${"Tokens".padEnd(12)} Runs`);
|
|
5179
5961
|
lines.push(` ${"\u2500".repeat(40)} ${"\u2500".repeat(12)} ${"\u2500".repeat(12)} ${"\u2500".repeat(6)}`);
|
|
5180
5962
|
for (const [model, data] of modelEntries) {
|
|
@@ -5183,7 +5965,7 @@ function formatCostsTerminal(summary) {
|
|
|
5183
5965
|
}
|
|
5184
5966
|
if (summary.byScenario.length > 0) {
|
|
5185
5967
|
lines.push("");
|
|
5186
|
-
lines.push(
|
|
5968
|
+
lines.push(chalk4.bold(" Top Scenarios by Cost"));
|
|
5187
5969
|
lines.push(` ${"Scenario".padEnd(40)} ${"Cost".padEnd(12)} ${"Tokens".padEnd(12)} Runs`);
|
|
5188
5970
|
lines.push(` ${"\u2500".repeat(40)} ${"\u2500".repeat(12)} ${"\u2500".repeat(12)} ${"\u2500".repeat(6)}`);
|
|
5189
5971
|
for (const s of summary.byScenario) {
|
|
@@ -5352,7 +6134,7 @@ function listTemplateNames() {
|
|
|
5352
6134
|
|
|
5353
6135
|
// src/db/auth-presets.ts
|
|
5354
6136
|
init_database();
|
|
5355
|
-
function
|
|
6137
|
+
function fromRow2(row) {
|
|
5356
6138
|
return {
|
|
5357
6139
|
id: row.id,
|
|
5358
6140
|
name: row.name,
|
|
@@ -5376,12 +6158,12 @@ function createAuthPreset(input) {
|
|
|
5376
6158
|
function getAuthPreset(name) {
|
|
5377
6159
|
const db2 = getDatabase();
|
|
5378
6160
|
const row = db2.query("SELECT * FROM auth_presets WHERE name = ?").get(name);
|
|
5379
|
-
return row ?
|
|
6161
|
+
return row ? fromRow2(row) : null;
|
|
5380
6162
|
}
|
|
5381
6163
|
function listAuthPresets() {
|
|
5382
6164
|
const db2 = getDatabase();
|
|
5383
6165
|
const rows = db2.query("SELECT * FROM auth_presets ORDER BY created_at DESC").all();
|
|
5384
|
-
return rows.map(
|
|
6166
|
+
return rows.map(fromRow2);
|
|
5385
6167
|
}
|
|
5386
6168
|
function deleteAuthPreset(name) {
|
|
5387
6169
|
const db2 = getDatabase();
|
|
@@ -5390,7 +6172,156 @@ function deleteAuthPreset(name) {
|
|
|
5390
6172
|
}
|
|
5391
6173
|
|
|
5392
6174
|
// src/cli/index.tsx
|
|
5393
|
-
|
|
6175
|
+
init_flows();
|
|
6176
|
+
|
|
6177
|
+
// src/db/environments.ts
|
|
6178
|
+
init_database();
|
|
6179
|
+
function fromRow3(row) {
|
|
6180
|
+
return {
|
|
6181
|
+
id: row.id,
|
|
6182
|
+
name: row.name,
|
|
6183
|
+
url: row.url,
|
|
6184
|
+
authPresetName: row.auth_preset_name,
|
|
6185
|
+
projectId: row.project_id,
|
|
6186
|
+
isDefault: row.is_default === 1,
|
|
6187
|
+
createdAt: row.created_at
|
|
6188
|
+
};
|
|
6189
|
+
}
|
|
6190
|
+
function createEnvironment(input) {
|
|
6191
|
+
const db2 = getDatabase();
|
|
6192
|
+
const id = uuid();
|
|
6193
|
+
const timestamp = now();
|
|
6194
|
+
db2.query(`
|
|
6195
|
+
INSERT INTO environments (id, name, url, auth_preset_name, project_id, is_default, metadata, created_at)
|
|
6196
|
+
VALUES (?, ?, ?, ?, ?, ?, '{}', ?)
|
|
6197
|
+
`).run(id, input.name, input.url, input.authPresetName ?? null, input.projectId ?? null, input.isDefault ? 1 : 0, timestamp);
|
|
6198
|
+
return getEnvironment(input.name);
|
|
6199
|
+
}
|
|
6200
|
+
function getEnvironment(name) {
|
|
6201
|
+
const db2 = getDatabase();
|
|
6202
|
+
const row = db2.query("SELECT * FROM environments WHERE name = ?").get(name);
|
|
6203
|
+
return row ? fromRow3(row) : null;
|
|
6204
|
+
}
|
|
6205
|
+
function listEnvironments(projectId) {
|
|
6206
|
+
const db2 = getDatabase();
|
|
6207
|
+
if (projectId) {
|
|
6208
|
+
const rows2 = db2.query("SELECT * FROM environments WHERE project_id = ? ORDER BY is_default DESC, created_at DESC").all(projectId);
|
|
6209
|
+
return rows2.map(fromRow3);
|
|
6210
|
+
}
|
|
6211
|
+
const rows = db2.query("SELECT * FROM environments ORDER BY is_default DESC, created_at DESC").all();
|
|
6212
|
+
return rows.map(fromRow3);
|
|
6213
|
+
}
|
|
6214
|
+
function deleteEnvironment(name) {
|
|
6215
|
+
const db2 = getDatabase();
|
|
6216
|
+
const result = db2.query("DELETE FROM environments WHERE name = ?").run(name);
|
|
6217
|
+
return result.changes > 0;
|
|
6218
|
+
}
|
|
6219
|
+
function setDefaultEnvironment(name) {
|
|
6220
|
+
const db2 = getDatabase();
|
|
6221
|
+
db2.exec("UPDATE environments SET is_default = 0");
|
|
6222
|
+
const result = db2.query("UPDATE environments SET is_default = 1 WHERE name = ?").run(name);
|
|
6223
|
+
if (result.changes === 0) {
|
|
6224
|
+
throw new Error(`Environment not found: ${name}`);
|
|
6225
|
+
}
|
|
6226
|
+
}
|
|
6227
|
+
function getDefaultEnvironment() {
|
|
6228
|
+
const db2 = getDatabase();
|
|
6229
|
+
const row = db2.query("SELECT * FROM environments WHERE is_default = 1 LIMIT 1").get();
|
|
6230
|
+
return row ? fromRow3(row) : null;
|
|
6231
|
+
}
|
|
6232
|
+
|
|
6233
|
+
// src/lib/ci.ts
|
|
6234
|
+
function generateGitHubActionsWorkflow() {
|
|
6235
|
+
return `name: AI QA Tests
|
|
6236
|
+
on:
|
|
6237
|
+
pull_request:
|
|
6238
|
+
push:
|
|
6239
|
+
branches: [main]
|
|
6240
|
+
|
|
6241
|
+
jobs:
|
|
6242
|
+
test:
|
|
6243
|
+
runs-on: ubuntu-latest
|
|
6244
|
+
steps:
|
|
6245
|
+
- uses: actions/checkout@v4
|
|
6246
|
+
- uses: oven-sh/setup-bun@v2
|
|
6247
|
+
- run: bun install -g @hasna/testers
|
|
6248
|
+
- run: testers install-browser
|
|
6249
|
+
- run: testers run \${{ env.TEST_URL }} --json --output results.json
|
|
6250
|
+
env:
|
|
6251
|
+
ANTHROPIC_API_KEY: \${{ secrets.ANTHROPIC_API_KEY }}
|
|
6252
|
+
TEST_URL: \${{ vars.TEST_URL || 'http://localhost:3000' }}
|
|
6253
|
+
- run: testers report --latest --output report.html
|
|
6254
|
+
- uses: actions/upload-artifact@v4
|
|
6255
|
+
if: always()
|
|
6256
|
+
with:
|
|
6257
|
+
name: test-report
|
|
6258
|
+
path: |
|
|
6259
|
+
report.html
|
|
6260
|
+
results.json
|
|
6261
|
+
`;
|
|
6262
|
+
}
|
|
6263
|
+
|
|
6264
|
+
// src/lib/assertions.ts
|
|
6265
|
+
function parseAssertionString(str) {
|
|
6266
|
+
const trimmed = str.trim();
|
|
6267
|
+
if (trimmed === "no-console-errors") {
|
|
6268
|
+
return { type: "no_console_errors", description: "No console errors" };
|
|
6269
|
+
}
|
|
6270
|
+
if (trimmed.startsWith("url:contains:")) {
|
|
6271
|
+
const expected = trimmed.slice("url:contains:".length);
|
|
6272
|
+
return { type: "url_contains", expected, description: `URL contains "${expected}"` };
|
|
6273
|
+
}
|
|
6274
|
+
if (trimmed.startsWith("title:contains:")) {
|
|
6275
|
+
const expected = trimmed.slice("title:contains:".length);
|
|
6276
|
+
return { type: "title_contains", expected, description: `Title contains "${expected}"` };
|
|
6277
|
+
}
|
|
6278
|
+
if (trimmed.startsWith("count:")) {
|
|
6279
|
+
const rest = trimmed.slice("count:".length);
|
|
6280
|
+
const eqIdx = rest.indexOf(" eq:");
|
|
6281
|
+
if (eqIdx === -1) {
|
|
6282
|
+
throw new Error(`Invalid count assertion format: ${str}. Expected "count:<selector> eq:<number>"`);
|
|
6283
|
+
}
|
|
6284
|
+
const selector = rest.slice(0, eqIdx);
|
|
6285
|
+
const expected = parseInt(rest.slice(eqIdx + " eq:".length), 10);
|
|
6286
|
+
return { type: "element_count", selector, expected, description: `${selector} count equals ${expected}` };
|
|
6287
|
+
}
|
|
6288
|
+
if (trimmed.startsWith("text:")) {
|
|
6289
|
+
const rest = trimmed.slice("text:".length);
|
|
6290
|
+
const containsIdx = rest.indexOf(" contains:");
|
|
6291
|
+
const equalsIdx = rest.indexOf(" equals:");
|
|
6292
|
+
if (containsIdx !== -1) {
|
|
6293
|
+
const selector = rest.slice(0, containsIdx);
|
|
6294
|
+
const expected = rest.slice(containsIdx + " contains:".length);
|
|
6295
|
+
return { type: "text_contains", selector, expected, description: `${selector} text contains "${expected}"` };
|
|
6296
|
+
}
|
|
6297
|
+
if (equalsIdx !== -1) {
|
|
6298
|
+
const selector = rest.slice(0, equalsIdx);
|
|
6299
|
+
const expected = rest.slice(equalsIdx + " equals:".length);
|
|
6300
|
+
return { type: "text_equals", selector, expected, description: `${selector} text equals "${expected}"` };
|
|
6301
|
+
}
|
|
6302
|
+
throw new Error(`Invalid text assertion format: ${str}. Expected "text:<selector> contains:<text>" or "text:<selector> equals:<text>"`);
|
|
6303
|
+
}
|
|
6304
|
+
if (trimmed.startsWith("selector:")) {
|
|
6305
|
+
const rest = trimmed.slice("selector:".length);
|
|
6306
|
+
const lastSpace = rest.lastIndexOf(" ");
|
|
6307
|
+
if (lastSpace === -1) {
|
|
6308
|
+
throw new Error(`Invalid selector assertion format: ${str}. Expected "selector:<selector> visible" or "selector:<selector> not-visible"`);
|
|
6309
|
+
}
|
|
6310
|
+
const selector = rest.slice(0, lastSpace);
|
|
6311
|
+
const action = rest.slice(lastSpace + 1);
|
|
6312
|
+
if (action === "visible") {
|
|
6313
|
+
return { type: "visible", selector, description: `${selector} is visible` };
|
|
6314
|
+
}
|
|
6315
|
+
if (action === "not-visible") {
|
|
6316
|
+
return { type: "not_visible", selector, description: `${selector} is not visible` };
|
|
6317
|
+
}
|
|
6318
|
+
throw new Error(`Unknown selector action: "${action}". Expected "visible" or "not-visible"`);
|
|
6319
|
+
}
|
|
6320
|
+
throw new Error(`Cannot parse assertion: "${str}". See --help for assertion formats.`);
|
|
6321
|
+
}
|
|
6322
|
+
|
|
6323
|
+
// src/cli/index.tsx
|
|
6324
|
+
import { existsSync as existsSync8, mkdirSync as mkdirSync4 } from "fs";
|
|
5394
6325
|
function formatToolInput(input) {
|
|
5395
6326
|
const parts = [];
|
|
5396
6327
|
for (const [key, value] of Object.entries(input)) {
|
|
@@ -5406,8 +6337,8 @@ var CONFIG_DIR2 = join6(process.env["HOME"] ?? "~", ".testers");
|
|
|
5406
6337
|
var CONFIG_PATH2 = join6(CONFIG_DIR2, "config.json");
|
|
5407
6338
|
function getActiveProject() {
|
|
5408
6339
|
try {
|
|
5409
|
-
if (
|
|
5410
|
-
const raw = JSON.parse(
|
|
6340
|
+
if (existsSync8(CONFIG_PATH2)) {
|
|
6341
|
+
const raw = JSON.parse(readFileSync6(CONFIG_PATH2, "utf-8"));
|
|
5411
6342
|
return raw.activeProject ?? undefined;
|
|
5412
6343
|
}
|
|
5413
6344
|
} catch {}
|
|
@@ -5422,21 +6353,25 @@ program2.command("add <name>").description("Create a new test scenario").option(
|
|
|
5422
6353
|
}, []).option("-t, --tag <tag>", "Tag (repeatable)", (val, acc) => {
|
|
5423
6354
|
acc.push(val);
|
|
5424
6355
|
return acc;
|
|
5425
|
-
}, []).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)").
|
|
6356
|
+
}, []).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) => {
|
|
6357
|
+
acc.push(val);
|
|
6358
|
+
return acc;
|
|
6359
|
+
}, []).action((name, opts) => {
|
|
5426
6360
|
try {
|
|
5427
6361
|
if (opts.template) {
|
|
5428
6362
|
const template = getTemplate(opts.template);
|
|
5429
6363
|
if (!template) {
|
|
5430
|
-
console.error(
|
|
6364
|
+
console.error(chalk5.red(`Unknown template: ${opts.template}. Available: ${listTemplateNames().join(", ")}`));
|
|
5431
6365
|
process.exit(1);
|
|
5432
6366
|
}
|
|
5433
6367
|
const projectId2 = resolveProject(opts.project);
|
|
5434
6368
|
for (const input of template) {
|
|
5435
6369
|
const s = createScenario({ ...input, projectId: projectId2 });
|
|
5436
|
-
console.log(
|
|
6370
|
+
console.log(chalk5.green(` Created ${s.shortId}: ${s.name}`));
|
|
5437
6371
|
}
|
|
5438
6372
|
return;
|
|
5439
6373
|
}
|
|
6374
|
+
const assertions = opts.assert.map(parseAssertionString);
|
|
5440
6375
|
const projectId = resolveProject(opts.project);
|
|
5441
6376
|
const scenario = createScenario({
|
|
5442
6377
|
name,
|
|
@@ -5448,11 +6383,12 @@ program2.command("add <name>").description("Create a new test scenario").option(
|
|
|
5448
6383
|
targetPath: opts.path,
|
|
5449
6384
|
requiresAuth: opts.auth,
|
|
5450
6385
|
timeoutMs: opts.timeout ? parseInt(opts.timeout, 10) : undefined,
|
|
6386
|
+
assertions: assertions.length > 0 ? assertions : undefined,
|
|
5451
6387
|
projectId
|
|
5452
6388
|
});
|
|
5453
|
-
console.log(
|
|
6389
|
+
console.log(chalk5.green(`Created scenario ${chalk5.bold(scenario.shortId)}: ${scenario.name}`));
|
|
5454
6390
|
} catch (error) {
|
|
5455
|
-
console.error(
|
|
6391
|
+
console.error(chalk5.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
5456
6392
|
process.exit(1);
|
|
5457
6393
|
}
|
|
5458
6394
|
});
|
|
@@ -5466,7 +6402,7 @@ program2.command("list").description("List test scenarios").option("-t, --tag <t
|
|
|
5466
6402
|
});
|
|
5467
6403
|
console.log(formatScenarioList(scenarios));
|
|
5468
6404
|
} catch (error) {
|
|
5469
|
-
console.error(
|
|
6405
|
+
console.error(chalk5.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
5470
6406
|
process.exit(1);
|
|
5471
6407
|
}
|
|
5472
6408
|
});
|
|
@@ -5474,33 +6410,33 @@ program2.command("show <id>").description("Show scenario details").action((id) =
|
|
|
5474
6410
|
try {
|
|
5475
6411
|
const scenario = getScenario(id) ?? getScenarioByShortId(id);
|
|
5476
6412
|
if (!scenario) {
|
|
5477
|
-
console.error(
|
|
6413
|
+
console.error(chalk5.red(`Scenario not found: ${id}`));
|
|
5478
6414
|
process.exit(1);
|
|
5479
6415
|
}
|
|
5480
6416
|
console.log("");
|
|
5481
|
-
console.log(
|
|
6417
|
+
console.log(chalk5.bold(` Scenario ${scenario.shortId}`));
|
|
5482
6418
|
console.log(` Name: ${scenario.name}`);
|
|
5483
|
-
console.log(` ID: ${
|
|
6419
|
+
console.log(` ID: ${chalk5.dim(scenario.id)}`);
|
|
5484
6420
|
console.log(` Description: ${scenario.description}`);
|
|
5485
6421
|
console.log(` Priority: ${scenario.priority}`);
|
|
5486
|
-
console.log(` Model: ${scenario.model ??
|
|
5487
|
-
console.log(` Tags: ${scenario.tags.length > 0 ? scenario.tags.join(", ") :
|
|
5488
|
-
console.log(` Path: ${scenario.targetPath ??
|
|
6422
|
+
console.log(` Model: ${scenario.model ?? chalk5.dim("default")}`);
|
|
6423
|
+
console.log(` Tags: ${scenario.tags.length > 0 ? scenario.tags.join(", ") : chalk5.dim("none")}`);
|
|
6424
|
+
console.log(` Path: ${scenario.targetPath ?? chalk5.dim("none")}`);
|
|
5489
6425
|
console.log(` Auth: ${scenario.requiresAuth ? "yes" : "no"}`);
|
|
5490
|
-
console.log(` Timeout: ${scenario.timeoutMs ? `${scenario.timeoutMs}ms` :
|
|
6426
|
+
console.log(` Timeout: ${scenario.timeoutMs ? `${scenario.timeoutMs}ms` : chalk5.dim("default")}`);
|
|
5491
6427
|
console.log(` Version: ${scenario.version}`);
|
|
5492
6428
|
console.log(` Created: ${scenario.createdAt}`);
|
|
5493
6429
|
console.log(` Updated: ${scenario.updatedAt}`);
|
|
5494
6430
|
if (scenario.steps.length > 0) {
|
|
5495
6431
|
console.log("");
|
|
5496
|
-
console.log(
|
|
6432
|
+
console.log(chalk5.bold(" Steps:"));
|
|
5497
6433
|
for (let i = 0;i < scenario.steps.length; i++) {
|
|
5498
6434
|
console.log(` ${i + 1}. ${scenario.steps[i]}`);
|
|
5499
6435
|
}
|
|
5500
6436
|
}
|
|
5501
6437
|
console.log("");
|
|
5502
6438
|
} catch (error) {
|
|
5503
|
-
console.error(
|
|
6439
|
+
console.error(chalk5.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
5504
6440
|
process.exit(1);
|
|
5505
6441
|
}
|
|
5506
6442
|
});
|
|
@@ -5514,7 +6450,7 @@ program2.command("update <id>").description("Update a scenario").option("-n, --n
|
|
|
5514
6450
|
try {
|
|
5515
6451
|
const scenario = getScenario(id) ?? getScenarioByShortId(id);
|
|
5516
6452
|
if (!scenario) {
|
|
5517
|
-
console.error(
|
|
6453
|
+
console.error(chalk5.red(`Scenario not found: ${id}`));
|
|
5518
6454
|
process.exit(1);
|
|
5519
6455
|
}
|
|
5520
6456
|
const updated = updateScenario(scenario.id, {
|
|
@@ -5525,9 +6461,9 @@ program2.command("update <id>").description("Update a scenario").option("-n, --n
|
|
|
5525
6461
|
priority: opts.priority,
|
|
5526
6462
|
model: opts.model
|
|
5527
6463
|
}, scenario.version);
|
|
5528
|
-
console.log(
|
|
6464
|
+
console.log(chalk5.green(`Updated scenario ${chalk5.bold(updated.shortId)}: ${updated.name}`));
|
|
5529
6465
|
} catch (error) {
|
|
5530
|
-
console.error(
|
|
6466
|
+
console.error(chalk5.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
5531
6467
|
process.exit(1);
|
|
5532
6468
|
}
|
|
5533
6469
|
});
|
|
@@ -5535,30 +6471,50 @@ program2.command("delete <id>").description("Delete a scenario").action((id) =>
|
|
|
5535
6471
|
try {
|
|
5536
6472
|
const scenario = getScenario(id) ?? getScenarioByShortId(id);
|
|
5537
6473
|
if (!scenario) {
|
|
5538
|
-
console.error(
|
|
6474
|
+
console.error(chalk5.red(`Scenario not found: ${id}`));
|
|
5539
6475
|
process.exit(1);
|
|
5540
6476
|
}
|
|
5541
6477
|
const deleted = deleteScenario(scenario.id);
|
|
5542
6478
|
if (deleted) {
|
|
5543
|
-
console.log(
|
|
6479
|
+
console.log(chalk5.green(`Deleted scenario ${scenario.shortId}: ${scenario.name}`));
|
|
5544
6480
|
} else {
|
|
5545
|
-
console.error(
|
|
6481
|
+
console.error(chalk5.red(`Failed to delete scenario: ${id}`));
|
|
5546
6482
|
process.exit(1);
|
|
5547
6483
|
}
|
|
5548
6484
|
} catch (error) {
|
|
5549
|
-
console.error(
|
|
6485
|
+
console.error(chalk5.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
5550
6486
|
process.exit(1);
|
|
5551
6487
|
}
|
|
5552
6488
|
});
|
|
5553
|
-
program2.command("run
|
|
6489
|
+
program2.command("run [url] [description]").description("Run test scenarios against a URL").option("-t, --tag <tag>", "Filter by tag (repeatable)", (val, acc) => {
|
|
5554
6490
|
acc.push(val);
|
|
5555
6491
|
return acc;
|
|
5556
|
-
}, []).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).action(async (
|
|
6492
|
+
}, []).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("--env <name>", "Use a named environment for the URL").action(async (urlArg, description, opts) => {
|
|
5557
6493
|
try {
|
|
5558
6494
|
const projectId = resolveProject(opts.project);
|
|
6495
|
+
let url = urlArg;
|
|
6496
|
+
if (!url && opts.env) {
|
|
6497
|
+
const env = getEnvironment(opts.env);
|
|
6498
|
+
if (!env) {
|
|
6499
|
+
console.error(chalk5.red(`Environment not found: ${opts.env}`));
|
|
6500
|
+
process.exit(1);
|
|
6501
|
+
}
|
|
6502
|
+
url = env.url;
|
|
6503
|
+
}
|
|
6504
|
+
if (!url) {
|
|
6505
|
+
const defaultEnv = getDefaultEnvironment();
|
|
6506
|
+
if (defaultEnv) {
|
|
6507
|
+
url = defaultEnv.url;
|
|
6508
|
+
console.log(chalk5.dim(`Using default environment: ${defaultEnv.name} (${defaultEnv.url})`));
|
|
6509
|
+
}
|
|
6510
|
+
}
|
|
6511
|
+
if (!url) {
|
|
6512
|
+
console.error(chalk5.red("No URL provided. Pass a URL argument, use --env <name>, or set a default environment with 'testers env use <name>'."));
|
|
6513
|
+
process.exit(1);
|
|
6514
|
+
}
|
|
5559
6515
|
if (opts.fromTodos) {
|
|
5560
6516
|
const result = importFromTodos({ projectId });
|
|
5561
|
-
console.log(
|
|
6517
|
+
console.log(chalk5.blue(`Imported ${result.imported} scenarios from todos (${result.skipped} skipped)`));
|
|
5562
6518
|
}
|
|
5563
6519
|
if (opts.background) {
|
|
5564
6520
|
if (description) {
|
|
@@ -5575,51 +6531,51 @@ program2.command("run <url> [description]").description("Run test scenarios agai
|
|
|
5575
6531
|
timeout: opts.timeout ? parseInt(opts.timeout, 10) : undefined,
|
|
5576
6532
|
projectId
|
|
5577
6533
|
});
|
|
5578
|
-
console.log(
|
|
5579
|
-
console.log(
|
|
5580
|
-
console.log(
|
|
5581
|
-
console.log(
|
|
6534
|
+
console.log(chalk5.green(`Run started in background: ${chalk5.bold(runId.slice(0, 8))}`));
|
|
6535
|
+
console.log(chalk5.dim(` Scenarios: ${scenarioCount}`));
|
|
6536
|
+
console.log(chalk5.dim(` URL: ${url}`));
|
|
6537
|
+
console.log(chalk5.dim(` Check progress: testers results ${runId.slice(0, 8)}`));
|
|
5582
6538
|
process.exit(0);
|
|
5583
6539
|
}
|
|
5584
6540
|
if (!opts.json && !opts.output) {
|
|
5585
6541
|
onRunEvent((event) => {
|
|
5586
6542
|
switch (event.type) {
|
|
5587
6543
|
case "scenario:start":
|
|
5588
|
-
console.log(
|
|
6544
|
+
console.log(chalk5.blue(` [start] ${event.scenarioName ?? event.scenarioId}`));
|
|
5589
6545
|
break;
|
|
5590
6546
|
case "step:thinking":
|
|
5591
6547
|
if (event.thinking) {
|
|
5592
6548
|
const preview = event.thinking.length > 120 ? event.thinking.slice(0, 120) + "..." : event.thinking;
|
|
5593
|
-
console.log(
|
|
6549
|
+
console.log(chalk5.dim(` [think] ${preview}`));
|
|
5594
6550
|
}
|
|
5595
6551
|
break;
|
|
5596
6552
|
case "step:tool_call":
|
|
5597
|
-
console.log(
|
|
6553
|
+
console.log(chalk5.cyan(` [step ${event.stepNumber}] ${event.toolName}${event.toolInput ? ` ${formatToolInput(event.toolInput)}` : ""}`));
|
|
5598
6554
|
break;
|
|
5599
6555
|
case "step:tool_result":
|
|
5600
6556
|
if (event.toolName === "report_result") {
|
|
5601
|
-
console.log(
|
|
6557
|
+
console.log(chalk5.bold(` [result] ${event.toolResult}`));
|
|
5602
6558
|
} else {
|
|
5603
6559
|
const resultPreview = (event.toolResult ?? "").length > 100 ? (event.toolResult ?? "").slice(0, 100) + "..." : event.toolResult ?? "";
|
|
5604
|
-
console.log(
|
|
6560
|
+
console.log(chalk5.dim(` [done] ${resultPreview}`));
|
|
5605
6561
|
}
|
|
5606
6562
|
break;
|
|
5607
6563
|
case "screenshot:captured":
|
|
5608
|
-
console.log(
|
|
6564
|
+
console.log(chalk5.dim(` [screenshot] ${event.screenshotPath}`));
|
|
5609
6565
|
break;
|
|
5610
6566
|
case "scenario:pass":
|
|
5611
|
-
console.log(
|
|
6567
|
+
console.log(chalk5.green(` [PASS] ${event.scenarioName}`));
|
|
5612
6568
|
break;
|
|
5613
6569
|
case "scenario:fail":
|
|
5614
|
-
console.log(
|
|
6570
|
+
console.log(chalk5.red(` [FAIL] ${event.scenarioName}`));
|
|
5615
6571
|
break;
|
|
5616
6572
|
case "scenario:error":
|
|
5617
|
-
console.log(
|
|
6573
|
+
console.log(chalk5.yellow(` [ERR] ${event.scenarioName}: ${event.error}`));
|
|
5618
6574
|
break;
|
|
5619
6575
|
}
|
|
5620
6576
|
});
|
|
5621
6577
|
console.log("");
|
|
5622
|
-
console.log(
|
|
6578
|
+
console.log(chalk5.bold(` Running tests against ${url}`));
|
|
5623
6579
|
console.log("");
|
|
5624
6580
|
}
|
|
5625
6581
|
if (description) {
|
|
@@ -5642,7 +6598,7 @@ program2.command("run <url> [description]").description("Run test scenarios agai
|
|
|
5642
6598
|
const jsonOutput = formatJSON(run2, results2);
|
|
5643
6599
|
if (opts.output) {
|
|
5644
6600
|
writeFileSync3(opts.output, jsonOutput, "utf-8");
|
|
5645
|
-
console.log(
|
|
6601
|
+
console.log(chalk5.green(`Results written to ${opts.output}`));
|
|
5646
6602
|
}
|
|
5647
6603
|
if (opts.json) {
|
|
5648
6604
|
console.log(jsonOutput);
|
|
@@ -5667,7 +6623,7 @@ program2.command("run <url> [description]").description("Run test scenarios agai
|
|
|
5667
6623
|
const jsonOutput = formatJSON(run, results);
|
|
5668
6624
|
if (opts.output) {
|
|
5669
6625
|
writeFileSync3(opts.output, jsonOutput, "utf-8");
|
|
5670
|
-
console.log(
|
|
6626
|
+
console.log(chalk5.green(`Results written to ${opts.output}`));
|
|
5671
6627
|
}
|
|
5672
6628
|
if (opts.json) {
|
|
5673
6629
|
console.log(jsonOutput);
|
|
@@ -5677,7 +6633,7 @@ program2.command("run <url> [description]").description("Run test scenarios agai
|
|
|
5677
6633
|
}
|
|
5678
6634
|
process.exit(getExitCode(run));
|
|
5679
6635
|
} catch (error) {
|
|
5680
|
-
console.error(
|
|
6636
|
+
console.error(chalk5.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
5681
6637
|
process.exit(1);
|
|
5682
6638
|
}
|
|
5683
6639
|
});
|
|
@@ -5689,7 +6645,7 @@ program2.command("runs").description("List past test runs").option("--status <st
|
|
|
5689
6645
|
});
|
|
5690
6646
|
console.log(formatRunList(runs));
|
|
5691
6647
|
} catch (error) {
|
|
5692
|
-
console.error(
|
|
6648
|
+
console.error(chalk5.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
5693
6649
|
process.exit(1);
|
|
5694
6650
|
}
|
|
5695
6651
|
});
|
|
@@ -5697,13 +6653,13 @@ program2.command("results <run-id>").description("Show results for a test run").
|
|
|
5697
6653
|
try {
|
|
5698
6654
|
const run = getRun(runId);
|
|
5699
6655
|
if (!run) {
|
|
5700
|
-
console.error(
|
|
6656
|
+
console.error(chalk5.red(`Run not found: ${runId}`));
|
|
5701
6657
|
process.exit(1);
|
|
5702
6658
|
}
|
|
5703
6659
|
const results = getResultsByRun(run.id);
|
|
5704
6660
|
console.log(formatTerminal(run, results));
|
|
5705
6661
|
} catch (error) {
|
|
5706
|
-
console.error(
|
|
6662
|
+
console.error(chalk5.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
5707
6663
|
process.exit(1);
|
|
5708
6664
|
}
|
|
5709
6665
|
});
|
|
@@ -5714,23 +6670,23 @@ program2.command("screenshots <id>").description("List screenshots for a run or
|
|
|
5714
6670
|
const results = getResultsByRun(run.id);
|
|
5715
6671
|
let total = 0;
|
|
5716
6672
|
console.log("");
|
|
5717
|
-
console.log(
|
|
6673
|
+
console.log(chalk5.bold(` Screenshots for run ${run.id.slice(0, 8)}`));
|
|
5718
6674
|
console.log("");
|
|
5719
6675
|
for (const result of results) {
|
|
5720
6676
|
const screenshots2 = listScreenshots(result.id);
|
|
5721
6677
|
if (screenshots2.length > 0) {
|
|
5722
6678
|
const scenario = getScenario(result.scenarioId);
|
|
5723
6679
|
const label = scenario ? `${scenario.shortId}: ${scenario.name}` : result.scenarioId.slice(0, 8);
|
|
5724
|
-
console.log(
|
|
6680
|
+
console.log(chalk5.bold(` ${label}`));
|
|
5725
6681
|
for (const ss of screenshots2) {
|
|
5726
|
-
console.log(` ${
|
|
6682
|
+
console.log(` ${chalk5.dim(String(ss.stepNumber).padStart(3, "0"))} ${ss.action} \u2014 ${chalk5.dim(ss.filePath)}`);
|
|
5727
6683
|
total++;
|
|
5728
6684
|
}
|
|
5729
6685
|
console.log("");
|
|
5730
6686
|
}
|
|
5731
6687
|
}
|
|
5732
6688
|
if (total === 0) {
|
|
5733
|
-
console.log(
|
|
6689
|
+
console.log(chalk5.dim(" No screenshots found."));
|
|
5734
6690
|
console.log("");
|
|
5735
6691
|
}
|
|
5736
6692
|
return;
|
|
@@ -5738,18 +6694,18 @@ program2.command("screenshots <id>").description("List screenshots for a run or
|
|
|
5738
6694
|
const screenshots = listScreenshots(id);
|
|
5739
6695
|
if (screenshots.length > 0) {
|
|
5740
6696
|
console.log("");
|
|
5741
|
-
console.log(
|
|
6697
|
+
console.log(chalk5.bold(` Screenshots for result ${id.slice(0, 8)}`));
|
|
5742
6698
|
console.log("");
|
|
5743
6699
|
for (const ss of screenshots) {
|
|
5744
|
-
console.log(` ${
|
|
6700
|
+
console.log(` ${chalk5.dim(String(ss.stepNumber).padStart(3, "0"))} ${ss.action} \u2014 ${chalk5.dim(ss.filePath)}`);
|
|
5745
6701
|
}
|
|
5746
6702
|
console.log("");
|
|
5747
6703
|
return;
|
|
5748
6704
|
}
|
|
5749
|
-
console.error(
|
|
6705
|
+
console.error(chalk5.red(`No screenshots found for: ${id}`));
|
|
5750
6706
|
process.exit(1);
|
|
5751
6707
|
} catch (error) {
|
|
5752
|
-
console.error(
|
|
6708
|
+
console.error(chalk5.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
5753
6709
|
process.exit(1);
|
|
5754
6710
|
}
|
|
5755
6711
|
});
|
|
@@ -5758,12 +6714,12 @@ program2.command("import <dir>").description("Import markdown test files as scen
|
|
|
5758
6714
|
const absDir = resolve(dir);
|
|
5759
6715
|
const files = readdirSync(absDir).filter((f) => f.endsWith(".md"));
|
|
5760
6716
|
if (files.length === 0) {
|
|
5761
|
-
console.log(
|
|
6717
|
+
console.log(chalk5.dim("No .md files found in directory."));
|
|
5762
6718
|
return;
|
|
5763
6719
|
}
|
|
5764
6720
|
let imported = 0;
|
|
5765
6721
|
for (const file of files) {
|
|
5766
|
-
const content =
|
|
6722
|
+
const content = readFileSync6(join6(absDir, file), "utf-8");
|
|
5767
6723
|
const lines = content.split(`
|
|
5768
6724
|
`);
|
|
5769
6725
|
let name = file.replace(/\.md$/, "");
|
|
@@ -5788,13 +6744,13 @@ program2.command("import <dir>").description("Import markdown test files as scen
|
|
|
5788
6744
|
description: descriptionLines.join(" ") || name,
|
|
5789
6745
|
steps
|
|
5790
6746
|
});
|
|
5791
|
-
console.log(
|
|
6747
|
+
console.log(chalk5.green(` Imported ${chalk5.bold(scenario.shortId)}: ${scenario.name}`));
|
|
5792
6748
|
imported++;
|
|
5793
6749
|
}
|
|
5794
6750
|
console.log("");
|
|
5795
|
-
console.log(
|
|
6751
|
+
console.log(chalk5.green(`Imported ${imported} scenario(s) from ${absDir}`));
|
|
5796
6752
|
} catch (error) {
|
|
5797
|
-
console.error(
|
|
6753
|
+
console.error(chalk5.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
5798
6754
|
process.exit(1);
|
|
5799
6755
|
}
|
|
5800
6756
|
});
|
|
@@ -5803,7 +6759,7 @@ program2.command("config").description("Show current configuration").action(() =
|
|
|
5803
6759
|
const config = loadConfig();
|
|
5804
6760
|
console.log(JSON.stringify(config, null, 2));
|
|
5805
6761
|
} catch (error) {
|
|
5806
|
-
console.error(
|
|
6762
|
+
console.error(chalk5.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
5807
6763
|
process.exit(1);
|
|
5808
6764
|
}
|
|
5809
6765
|
});
|
|
@@ -5813,25 +6769,25 @@ program2.command("status").description("Show database and auth status").action((
|
|
|
5813
6769
|
const hasApiKey = !!config.anthropicApiKey || !!process.env["ANTHROPIC_API_KEY"];
|
|
5814
6770
|
const dbPath = join6(process.env["HOME"] ?? "~", ".testers", "testers.db");
|
|
5815
6771
|
console.log("");
|
|
5816
|
-
console.log(
|
|
6772
|
+
console.log(chalk5.bold(" Open Testers Status"));
|
|
5817
6773
|
console.log("");
|
|
5818
|
-
console.log(` ANTHROPIC_API_KEY: ${hasApiKey ?
|
|
6774
|
+
console.log(` ANTHROPIC_API_KEY: ${hasApiKey ? chalk5.green("set") : chalk5.red("not set")}`);
|
|
5819
6775
|
console.log(` Database: ${dbPath}`);
|
|
5820
6776
|
console.log(` Default model: ${config.defaultModel}`);
|
|
5821
6777
|
console.log(` Screenshots dir: ${config.screenshots.dir}`);
|
|
5822
6778
|
console.log("");
|
|
5823
6779
|
} catch (error) {
|
|
5824
|
-
console.error(
|
|
6780
|
+
console.error(chalk5.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
5825
6781
|
process.exit(1);
|
|
5826
6782
|
}
|
|
5827
6783
|
});
|
|
5828
6784
|
program2.command("install-browser").description("Install Playwright Chromium browser").action(async () => {
|
|
5829
6785
|
try {
|
|
5830
|
-
console.log(
|
|
6786
|
+
console.log(chalk5.blue("Installing Playwright Chromium..."));
|
|
5831
6787
|
await installBrowser();
|
|
5832
|
-
console.log(
|
|
6788
|
+
console.log(chalk5.green("Browser installed successfully."));
|
|
5833
6789
|
} catch (error) {
|
|
5834
|
-
console.error(
|
|
6790
|
+
console.error(chalk5.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
5835
6791
|
process.exit(1);
|
|
5836
6792
|
}
|
|
5837
6793
|
});
|
|
@@ -5843,9 +6799,9 @@ projectCmd.command("create <name>").description("Create a new project").option("
|
|
|
5843
6799
|
path: opts.path,
|
|
5844
6800
|
description: opts.description
|
|
5845
6801
|
});
|
|
5846
|
-
console.log(
|
|
6802
|
+
console.log(chalk5.green(`Created project ${chalk5.bold(project.name)} (${project.id})`));
|
|
5847
6803
|
} catch (error) {
|
|
5848
|
-
console.error(
|
|
6804
|
+
console.error(chalk5.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
5849
6805
|
process.exit(1);
|
|
5850
6806
|
}
|
|
5851
6807
|
});
|
|
@@ -5853,20 +6809,20 @@ projectCmd.command("list").description("List all projects").action(() => {
|
|
|
5853
6809
|
try {
|
|
5854
6810
|
const projects = listProjects();
|
|
5855
6811
|
if (projects.length === 0) {
|
|
5856
|
-
console.log(
|
|
6812
|
+
console.log(chalk5.dim("No projects found."));
|
|
5857
6813
|
return;
|
|
5858
6814
|
}
|
|
5859
6815
|
console.log("");
|
|
5860
|
-
console.log(
|
|
6816
|
+
console.log(chalk5.bold(" Projects"));
|
|
5861
6817
|
console.log("");
|
|
5862
6818
|
console.log(` ${"ID".padEnd(38)} ${"Name".padEnd(24)} ${"Path".padEnd(30)} Created`);
|
|
5863
6819
|
console.log(` ${"\u2500".repeat(38)} ${"\u2500".repeat(24)} ${"\u2500".repeat(30)} ${"\u2500".repeat(20)}`);
|
|
5864
6820
|
for (const p of projects) {
|
|
5865
|
-
console.log(` ${p.id.padEnd(38)} ${p.name.padEnd(24)} ${(p.path ??
|
|
6821
|
+
console.log(` ${p.id.padEnd(38)} ${p.name.padEnd(24)} ${(p.path ?? chalk5.dim("\u2014")).toString().padEnd(30)} ${p.createdAt}`);
|
|
5866
6822
|
}
|
|
5867
6823
|
console.log("");
|
|
5868
6824
|
} catch (error) {
|
|
5869
|
-
console.error(
|
|
6825
|
+
console.error(chalk5.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
5870
6826
|
process.exit(1);
|
|
5871
6827
|
}
|
|
5872
6828
|
});
|
|
@@ -5874,39 +6830,39 @@ projectCmd.command("show <id>").description("Show project details").action((id)
|
|
|
5874
6830
|
try {
|
|
5875
6831
|
const project = getProject(id);
|
|
5876
6832
|
if (!project) {
|
|
5877
|
-
console.error(
|
|
6833
|
+
console.error(chalk5.red(`Project not found: ${id}`));
|
|
5878
6834
|
process.exit(1);
|
|
5879
6835
|
}
|
|
5880
6836
|
console.log("");
|
|
5881
|
-
console.log(
|
|
6837
|
+
console.log(chalk5.bold(` Project: ${project.name}`));
|
|
5882
6838
|
console.log(` ID: ${project.id}`);
|
|
5883
|
-
console.log(` Path: ${project.path ??
|
|
5884
|
-
console.log(` Description: ${project.description ??
|
|
6839
|
+
console.log(` Path: ${project.path ?? chalk5.dim("none")}`);
|
|
6840
|
+
console.log(` Description: ${project.description ?? chalk5.dim("none")}`);
|
|
5885
6841
|
console.log(` Created: ${project.createdAt}`);
|
|
5886
6842
|
console.log(` Updated: ${project.updatedAt}`);
|
|
5887
6843
|
console.log("");
|
|
5888
6844
|
} catch (error) {
|
|
5889
|
-
console.error(
|
|
6845
|
+
console.error(chalk5.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
5890
6846
|
process.exit(1);
|
|
5891
6847
|
}
|
|
5892
6848
|
});
|
|
5893
6849
|
projectCmd.command("use <name>").description("Set active project (find or create)").action((name) => {
|
|
5894
6850
|
try {
|
|
5895
6851
|
const project = ensureProject(name, process.cwd());
|
|
5896
|
-
if (!
|
|
6852
|
+
if (!existsSync8(CONFIG_DIR2)) {
|
|
5897
6853
|
mkdirSync4(CONFIG_DIR2, { recursive: true });
|
|
5898
6854
|
}
|
|
5899
6855
|
let config = {};
|
|
5900
|
-
if (
|
|
6856
|
+
if (existsSync8(CONFIG_PATH2)) {
|
|
5901
6857
|
try {
|
|
5902
|
-
config = JSON.parse(
|
|
6858
|
+
config = JSON.parse(readFileSync6(CONFIG_PATH2, "utf-8"));
|
|
5903
6859
|
} catch {}
|
|
5904
6860
|
}
|
|
5905
6861
|
config.activeProject = project.id;
|
|
5906
6862
|
writeFileSync3(CONFIG_PATH2, JSON.stringify(config, null, 2), "utf-8");
|
|
5907
|
-
console.log(
|
|
6863
|
+
console.log(chalk5.green(`Active project set to ${chalk5.bold(project.name)} (${project.id})`));
|
|
5908
6864
|
} catch (error) {
|
|
5909
|
-
console.error(
|
|
6865
|
+
console.error(chalk5.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
5910
6866
|
process.exit(1);
|
|
5911
6867
|
}
|
|
5912
6868
|
});
|
|
@@ -5931,12 +6887,12 @@ scheduleCmd.command("create <name>").description("Create a new schedule").requir
|
|
|
5931
6887
|
timeoutMs: opts.timeout ? parseInt(opts.timeout, 10) : undefined,
|
|
5932
6888
|
projectId
|
|
5933
6889
|
});
|
|
5934
|
-
console.log(
|
|
6890
|
+
console.log(chalk5.green(`Created schedule ${chalk5.bold(schedule.name)} (${schedule.id})`));
|
|
5935
6891
|
if (schedule.nextRunAt) {
|
|
5936
|
-
console.log(
|
|
6892
|
+
console.log(chalk5.dim(` Next run at: ${schedule.nextRunAt}`));
|
|
5937
6893
|
}
|
|
5938
6894
|
} catch (error) {
|
|
5939
|
-
console.error(
|
|
6895
|
+
console.error(chalk5.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
5940
6896
|
process.exit(1);
|
|
5941
6897
|
}
|
|
5942
6898
|
});
|
|
@@ -5948,23 +6904,23 @@ scheduleCmd.command("list").description("List schedules").option("--project <id>
|
|
|
5948
6904
|
enabled: opts.enabled ? true : undefined
|
|
5949
6905
|
});
|
|
5950
6906
|
if (schedules.length === 0) {
|
|
5951
|
-
console.log(
|
|
6907
|
+
console.log(chalk5.dim("No schedules found."));
|
|
5952
6908
|
return;
|
|
5953
6909
|
}
|
|
5954
6910
|
console.log("");
|
|
5955
|
-
console.log(
|
|
6911
|
+
console.log(chalk5.bold(" Schedules"));
|
|
5956
6912
|
console.log("");
|
|
5957
6913
|
console.log(` ${"Name".padEnd(20)} ${"Cron".padEnd(18)} ${"URL".padEnd(30)} ${"Enabled".padEnd(9)} ${"Next Run".padEnd(22)} Last Run`);
|
|
5958
6914
|
console.log(` ${"\u2500".repeat(20)} ${"\u2500".repeat(18)} ${"\u2500".repeat(30)} ${"\u2500".repeat(9)} ${"\u2500".repeat(22)} ${"\u2500".repeat(22)}`);
|
|
5959
6915
|
for (const s of schedules) {
|
|
5960
|
-
const enabled = s.enabled ?
|
|
5961
|
-
const nextRun = s.nextRunAt ??
|
|
5962
|
-
const lastRun = s.lastRunAt ??
|
|
6916
|
+
const enabled = s.enabled ? chalk5.green("yes") : chalk5.red("no");
|
|
6917
|
+
const nextRun = s.nextRunAt ?? chalk5.dim("\u2014");
|
|
6918
|
+
const lastRun = s.lastRunAt ?? chalk5.dim("\u2014");
|
|
5963
6919
|
console.log(` ${s.name.padEnd(20)} ${s.cronExpression.padEnd(18)} ${s.url.padEnd(30)} ${enabled.toString().padEnd(9)} ${nextRun.toString().padEnd(22)} ${lastRun}`);
|
|
5964
6920
|
}
|
|
5965
6921
|
console.log("");
|
|
5966
6922
|
} catch (error) {
|
|
5967
|
-
console.error(
|
|
6923
|
+
console.error(chalk5.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
5968
6924
|
process.exit(1);
|
|
5969
6925
|
}
|
|
5970
6926
|
});
|
|
@@ -5972,47 +6928,47 @@ scheduleCmd.command("show <id>").description("Show schedule details").action((id
|
|
|
5972
6928
|
try {
|
|
5973
6929
|
const schedule = getSchedule(id);
|
|
5974
6930
|
if (!schedule) {
|
|
5975
|
-
console.error(
|
|
6931
|
+
console.error(chalk5.red(`Schedule not found: ${id}`));
|
|
5976
6932
|
process.exit(1);
|
|
5977
6933
|
}
|
|
5978
6934
|
console.log("");
|
|
5979
|
-
console.log(
|
|
6935
|
+
console.log(chalk5.bold(` Schedule: ${schedule.name}`));
|
|
5980
6936
|
console.log(` ID: ${schedule.id}`);
|
|
5981
6937
|
console.log(` Cron: ${schedule.cronExpression}`);
|
|
5982
6938
|
console.log(` URL: ${schedule.url}`);
|
|
5983
|
-
console.log(` Enabled: ${schedule.enabled ?
|
|
5984
|
-
console.log(` Model: ${schedule.model ??
|
|
6939
|
+
console.log(` Enabled: ${schedule.enabled ? chalk5.green("yes") : chalk5.red("no")}`);
|
|
6940
|
+
console.log(` Model: ${schedule.model ?? chalk5.dim("default")}`);
|
|
5985
6941
|
console.log(` Headed: ${schedule.headed ? "yes" : "no"}`);
|
|
5986
6942
|
console.log(` Parallel: ${schedule.parallel}`);
|
|
5987
|
-
console.log(` Timeout: ${schedule.timeoutMs ? `${schedule.timeoutMs}ms` :
|
|
5988
|
-
console.log(` Project: ${schedule.projectId ??
|
|
6943
|
+
console.log(` Timeout: ${schedule.timeoutMs ? `${schedule.timeoutMs}ms` : chalk5.dim("default")}`);
|
|
6944
|
+
console.log(` Project: ${schedule.projectId ?? chalk5.dim("none")}`);
|
|
5989
6945
|
console.log(` Filter: ${JSON.stringify(schedule.scenarioFilter)}`);
|
|
5990
|
-
console.log(` Next run: ${schedule.nextRunAt ??
|
|
5991
|
-
console.log(` Last run: ${schedule.lastRunAt ??
|
|
5992
|
-
console.log(` Last run ID: ${schedule.lastRunId ??
|
|
6946
|
+
console.log(` Next run: ${schedule.nextRunAt ?? chalk5.dim("not scheduled")}`);
|
|
6947
|
+
console.log(` Last run: ${schedule.lastRunAt ?? chalk5.dim("never")}`);
|
|
6948
|
+
console.log(` Last run ID: ${schedule.lastRunId ?? chalk5.dim("none")}`);
|
|
5993
6949
|
console.log(` Created: ${schedule.createdAt}`);
|
|
5994
6950
|
console.log(` Updated: ${schedule.updatedAt}`);
|
|
5995
6951
|
console.log("");
|
|
5996
6952
|
} catch (error) {
|
|
5997
|
-
console.error(
|
|
6953
|
+
console.error(chalk5.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
5998
6954
|
process.exit(1);
|
|
5999
6955
|
}
|
|
6000
6956
|
});
|
|
6001
6957
|
scheduleCmd.command("enable <id>").description("Enable a schedule").action((id) => {
|
|
6002
6958
|
try {
|
|
6003
6959
|
const schedule = updateSchedule(id, { enabled: true });
|
|
6004
|
-
console.log(
|
|
6960
|
+
console.log(chalk5.green(`Enabled schedule ${chalk5.bold(schedule.name)}`));
|
|
6005
6961
|
} catch (error) {
|
|
6006
|
-
console.error(
|
|
6962
|
+
console.error(chalk5.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
6007
6963
|
process.exit(1);
|
|
6008
6964
|
}
|
|
6009
6965
|
});
|
|
6010
6966
|
scheduleCmd.command("disable <id>").description("Disable a schedule").action((id) => {
|
|
6011
6967
|
try {
|
|
6012
6968
|
const schedule = updateSchedule(id, { enabled: false });
|
|
6013
|
-
console.log(
|
|
6969
|
+
console.log(chalk5.green(`Disabled schedule ${chalk5.bold(schedule.name)}`));
|
|
6014
6970
|
} catch (error) {
|
|
6015
|
-
console.error(
|
|
6971
|
+
console.error(chalk5.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
6016
6972
|
process.exit(1);
|
|
6017
6973
|
}
|
|
6018
6974
|
});
|
|
@@ -6020,13 +6976,13 @@ scheduleCmd.command("delete <id>").description("Delete a schedule").action((id)
|
|
|
6020
6976
|
try {
|
|
6021
6977
|
const deleted = deleteSchedule(id);
|
|
6022
6978
|
if (deleted) {
|
|
6023
|
-
console.log(
|
|
6979
|
+
console.log(chalk5.green(`Deleted schedule: ${id}`));
|
|
6024
6980
|
} else {
|
|
6025
|
-
console.error(
|
|
6981
|
+
console.error(chalk5.red(`Schedule not found: ${id}`));
|
|
6026
6982
|
process.exit(1);
|
|
6027
6983
|
}
|
|
6028
6984
|
} catch (error) {
|
|
6029
|
-
console.error(
|
|
6985
|
+
console.error(chalk5.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
6030
6986
|
process.exit(1);
|
|
6031
6987
|
}
|
|
6032
6988
|
});
|
|
@@ -6034,11 +6990,11 @@ scheduleCmd.command("run <id>").description("Manually trigger a schedule").optio
|
|
|
6034
6990
|
try {
|
|
6035
6991
|
const schedule = getSchedule(id);
|
|
6036
6992
|
if (!schedule) {
|
|
6037
|
-
console.error(
|
|
6993
|
+
console.error(chalk5.red(`Schedule not found: ${id}`));
|
|
6038
6994
|
process.exit(1);
|
|
6039
6995
|
return;
|
|
6040
6996
|
}
|
|
6041
|
-
console.log(
|
|
6997
|
+
console.log(chalk5.blue(`Running schedule ${chalk5.bold(schedule.name)} against ${schedule.url}...`));
|
|
6042
6998
|
const { run, results } = await runByFilter({
|
|
6043
6999
|
url: schedule.url,
|
|
6044
7000
|
tags: schedule.scenarioFilter.tags,
|
|
@@ -6057,15 +7013,15 @@ scheduleCmd.command("run <id>").description("Manually trigger a schedule").optio
|
|
|
6057
7013
|
}
|
|
6058
7014
|
process.exit(getExitCode(run));
|
|
6059
7015
|
} catch (error) {
|
|
6060
|
-
console.error(
|
|
7016
|
+
console.error(chalk5.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
6061
7017
|
process.exit(1);
|
|
6062
7018
|
}
|
|
6063
7019
|
});
|
|
6064
7020
|
program2.command("daemon").description("Start the scheduler daemon").option("--interval <seconds>", "Check interval in seconds", "60").action(async (opts) => {
|
|
6065
7021
|
try {
|
|
6066
7022
|
const intervalMs = parseInt(opts.interval, 10) * 1000;
|
|
6067
|
-
console.log(
|
|
6068
|
-
console.log(
|
|
7023
|
+
console.log(chalk5.blue("Scheduler daemon started. Press Ctrl+C to stop."));
|
|
7024
|
+
console.log(chalk5.dim(` Check interval: ${opts.interval}s`));
|
|
6069
7025
|
let running = true;
|
|
6070
7026
|
const checkAndRun = async () => {
|
|
6071
7027
|
while (running) {
|
|
@@ -6074,7 +7030,7 @@ program2.command("daemon").description("Start the scheduler daemon").option("--i
|
|
|
6074
7030
|
const now2 = new Date().toISOString();
|
|
6075
7031
|
for (const schedule of schedules) {
|
|
6076
7032
|
if (schedule.nextRunAt && schedule.nextRunAt <= now2) {
|
|
6077
|
-
console.log(
|
|
7033
|
+
console.log(chalk5.blue(`[${new Date().toISOString()}] Triggering schedule: ${schedule.name}`));
|
|
6078
7034
|
try {
|
|
6079
7035
|
const { run } = await runByFilter({
|
|
6080
7036
|
url: schedule.url,
|
|
@@ -6087,39 +7043,39 @@ program2.command("daemon").description("Start the scheduler daemon").option("--i
|
|
|
6087
7043
|
timeout: schedule.timeoutMs ?? undefined,
|
|
6088
7044
|
projectId: schedule.projectId ?? undefined
|
|
6089
7045
|
});
|
|
6090
|
-
const statusColor = run.status === "passed" ?
|
|
7046
|
+
const statusColor = run.status === "passed" ? chalk5.green : chalk5.red;
|
|
6091
7047
|
console.log(` ${statusColor(run.status)} \u2014 ${run.passed}/${run.total} passed`);
|
|
6092
7048
|
updateSchedule(schedule.id, {});
|
|
6093
7049
|
} catch (err) {
|
|
6094
|
-
console.error(
|
|
7050
|
+
console.error(chalk5.red(` Error running schedule ${schedule.name}: ${err instanceof Error ? err.message : String(err)}`));
|
|
6095
7051
|
}
|
|
6096
7052
|
}
|
|
6097
7053
|
}
|
|
6098
7054
|
} catch (err) {
|
|
6099
|
-
console.error(
|
|
7055
|
+
console.error(chalk5.red(`Daemon error: ${err instanceof Error ? err.message : String(err)}`));
|
|
6100
7056
|
}
|
|
6101
7057
|
await new Promise((resolve2) => setTimeout(resolve2, intervalMs));
|
|
6102
7058
|
}
|
|
6103
7059
|
};
|
|
6104
7060
|
process.on("SIGINT", () => {
|
|
6105
|
-
console.log(
|
|
7061
|
+
console.log(chalk5.yellow(`
|
|
6106
7062
|
Shutting down scheduler daemon...`));
|
|
6107
7063
|
running = false;
|
|
6108
7064
|
process.exit(0);
|
|
6109
7065
|
});
|
|
6110
7066
|
process.on("SIGTERM", () => {
|
|
6111
|
-
console.log(
|
|
7067
|
+
console.log(chalk5.yellow(`
|
|
6112
7068
|
Shutting down scheduler daemon...`));
|
|
6113
7069
|
running = false;
|
|
6114
7070
|
process.exit(0);
|
|
6115
7071
|
});
|
|
6116
7072
|
await checkAndRun();
|
|
6117
7073
|
} catch (error) {
|
|
6118
|
-
console.error(
|
|
7074
|
+
console.error(chalk5.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
6119
7075
|
process.exit(1);
|
|
6120
7076
|
}
|
|
6121
7077
|
});
|
|
6122
|
-
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").action((opts) => {
|
|
7078
|
+
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) => {
|
|
6123
7079
|
try {
|
|
6124
7080
|
const { project, scenarios, framework } = initProject({
|
|
6125
7081
|
name: opts.name,
|
|
@@ -6127,30 +7083,41 @@ program2.command("init").description("Initialize a new testing project").option(
|
|
|
6127
7083
|
path: opts.path
|
|
6128
7084
|
});
|
|
6129
7085
|
console.log("");
|
|
6130
|
-
console.log(
|
|
7086
|
+
console.log(chalk5.bold(" Project initialized!"));
|
|
6131
7087
|
console.log("");
|
|
6132
7088
|
if (framework) {
|
|
6133
|
-
console.log(` Framework: ${
|
|
7089
|
+
console.log(` Framework: ${chalk5.cyan(framework.name)}`);
|
|
6134
7090
|
if (framework.features.length > 0) {
|
|
6135
|
-
console.log(` Features: ${
|
|
7091
|
+
console.log(` Features: ${chalk5.dim(framework.features.join(", "))}`);
|
|
6136
7092
|
}
|
|
6137
7093
|
} else {
|
|
6138
|
-
console.log(` Framework: ${
|
|
7094
|
+
console.log(` Framework: ${chalk5.dim("not detected")}`);
|
|
6139
7095
|
}
|
|
6140
|
-
console.log(` Project: ${
|
|
6141
|
-
console.log(` Scenarios: ${
|
|
7096
|
+
console.log(` Project: ${chalk5.green(project.name)} ${chalk5.dim(`(${project.id})`)}`);
|
|
7097
|
+
console.log(` Scenarios: ${chalk5.green(String(scenarios.length))} starter scenarios created`);
|
|
6142
7098
|
console.log("");
|
|
6143
7099
|
for (const s of scenarios) {
|
|
6144
|
-
console.log(` ${
|
|
7100
|
+
console.log(` ${chalk5.dim(s.shortId)} ${s.name} ${chalk5.dim(`[${s.tags.join(", ")}]`)}`);
|
|
7101
|
+
}
|
|
7102
|
+
if (opts.ci === "github") {
|
|
7103
|
+
const workflowDir = join6(process.cwd(), ".github", "workflows");
|
|
7104
|
+
if (!existsSync8(workflowDir)) {
|
|
7105
|
+
mkdirSync4(workflowDir, { recursive: true });
|
|
7106
|
+
}
|
|
7107
|
+
const workflowPath = join6(workflowDir, "testers.yml");
|
|
7108
|
+
writeFileSync3(workflowPath, generateGitHubActionsWorkflow(), "utf-8");
|
|
7109
|
+
console.log(` CI: ${chalk5.green("GitHub Actions workflow written to .github/workflows/testers.yml")}`);
|
|
7110
|
+
} else if (opts.ci) {
|
|
7111
|
+
console.log(chalk5.yellow(` Unknown CI provider: ${opts.ci}. Supported: github`));
|
|
6145
7112
|
}
|
|
6146
7113
|
console.log("");
|
|
6147
|
-
console.log(
|
|
7114
|
+
console.log(chalk5.bold(" Next steps:"));
|
|
6148
7115
|
console.log(` 1. Start your dev server`);
|
|
6149
|
-
console.log(` 2. Run ${
|
|
6150
|
-
console.log(` 3. Add more scenarios with ${
|
|
7116
|
+
console.log(` 2. Run ${chalk5.cyan("testers run <url>")} to execute tests`);
|
|
7117
|
+
console.log(` 3. Add more scenarios with ${chalk5.cyan("testers add <name>")}`);
|
|
6151
7118
|
console.log("");
|
|
6152
7119
|
} catch (error) {
|
|
6153
|
-
console.error(
|
|
7120
|
+
console.error(chalk5.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
6154
7121
|
process.exit(1);
|
|
6155
7122
|
}
|
|
6156
7123
|
});
|
|
@@ -6158,16 +7125,16 @@ program2.command("replay <run-id>").description("Re-run all scenarios from a pre
|
|
|
6158
7125
|
try {
|
|
6159
7126
|
const originalRun = getRun(runId);
|
|
6160
7127
|
if (!originalRun) {
|
|
6161
|
-
console.error(
|
|
7128
|
+
console.error(chalk5.red(`Run not found: ${runId}`));
|
|
6162
7129
|
process.exit(1);
|
|
6163
7130
|
}
|
|
6164
7131
|
const originalResults = getResultsByRun(originalRun.id);
|
|
6165
7132
|
const scenarioIds = originalResults.map((r) => r.scenarioId);
|
|
6166
7133
|
if (scenarioIds.length === 0) {
|
|
6167
|
-
console.log(
|
|
7134
|
+
console.log(chalk5.dim("No scenarios to replay."));
|
|
6168
7135
|
return;
|
|
6169
7136
|
}
|
|
6170
|
-
console.log(
|
|
7137
|
+
console.log(chalk5.blue(`Replaying ${scenarioIds.length} scenarios from run ${originalRun.id.slice(0, 8)}...`));
|
|
6171
7138
|
const { run, results } = await runByFilter({
|
|
6172
7139
|
url: opts.url ?? originalRun.url,
|
|
6173
7140
|
scenarioIds,
|
|
@@ -6182,7 +7149,7 @@ program2.command("replay <run-id>").description("Re-run all scenarios from a pre
|
|
|
6182
7149
|
}
|
|
6183
7150
|
process.exit(getExitCode(run));
|
|
6184
7151
|
} catch (error) {
|
|
6185
|
-
console.error(
|
|
7152
|
+
console.error(chalk5.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
6186
7153
|
process.exit(1);
|
|
6187
7154
|
}
|
|
6188
7155
|
});
|
|
@@ -6190,16 +7157,16 @@ program2.command("retry <run-id>").description("Re-run only failed scenarios fro
|
|
|
6190
7157
|
try {
|
|
6191
7158
|
const originalRun = getRun(runId);
|
|
6192
7159
|
if (!originalRun) {
|
|
6193
|
-
console.error(
|
|
7160
|
+
console.error(chalk5.red(`Run not found: ${runId}`));
|
|
6194
7161
|
process.exit(1);
|
|
6195
7162
|
}
|
|
6196
7163
|
const originalResults = getResultsByRun(originalRun.id);
|
|
6197
7164
|
const failedScenarioIds = originalResults.filter((r) => r.status === "failed" || r.status === "error").map((r) => r.scenarioId);
|
|
6198
7165
|
if (failedScenarioIds.length === 0) {
|
|
6199
|
-
console.log(
|
|
7166
|
+
console.log(chalk5.green("No failed scenarios to retry. All passed!"));
|
|
6200
7167
|
return;
|
|
6201
7168
|
}
|
|
6202
|
-
console.log(
|
|
7169
|
+
console.log(chalk5.blue(`Retrying ${failedScenarioIds.length} failed scenarios from run ${originalRun.id.slice(0, 8)}...`));
|
|
6203
7170
|
const { run, results } = await runByFilter({
|
|
6204
7171
|
url: opts.url ?? originalRun.url,
|
|
6205
7172
|
scenarioIds: failedScenarioIds,
|
|
@@ -6209,13 +7176,13 @@ program2.command("retry <run-id>").description("Re-run only failed scenarios fro
|
|
|
6209
7176
|
});
|
|
6210
7177
|
if (!opts.json) {
|
|
6211
7178
|
console.log("");
|
|
6212
|
-
console.log(
|
|
7179
|
+
console.log(chalk5.bold(" Comparison with original run:"));
|
|
6213
7180
|
for (const result of results) {
|
|
6214
7181
|
const original = originalResults.find((r) => r.scenarioId === result.scenarioId);
|
|
6215
7182
|
if (original) {
|
|
6216
7183
|
const changed = original.status !== result.status;
|
|
6217
|
-
const arrow = changed ?
|
|
6218
|
-
const icon = result.status === "passed" ?
|
|
7184
|
+
const arrow = changed ? chalk5.yellow(`${original.status} \u2192 ${result.status}`) : chalk5.dim(`${result.status} (unchanged)`);
|
|
7185
|
+
const icon = result.status === "passed" ? chalk5.green("\u2713") : chalk5.red("\u2717");
|
|
6219
7186
|
console.log(` ${icon} ${result.scenarioId.slice(0, 8)}: ${arrow}`);
|
|
6220
7187
|
}
|
|
6221
7188
|
}
|
|
@@ -6228,14 +7195,14 @@ program2.command("retry <run-id>").description("Re-run only failed scenarios fro
|
|
|
6228
7195
|
}
|
|
6229
7196
|
process.exit(getExitCode(run));
|
|
6230
7197
|
} catch (error) {
|
|
6231
|
-
console.error(
|
|
7198
|
+
console.error(chalk5.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
6232
7199
|
process.exit(1);
|
|
6233
7200
|
}
|
|
6234
7201
|
});
|
|
6235
7202
|
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) => {
|
|
6236
7203
|
try {
|
|
6237
7204
|
const projectId = resolveProject(opts.project);
|
|
6238
|
-
console.log(
|
|
7205
|
+
console.log(chalk5.blue(`Running smoke test against ${chalk5.bold(url)}...`));
|
|
6239
7206
|
console.log("");
|
|
6240
7207
|
const smokeResult = await runSmoke({
|
|
6241
7208
|
url,
|
|
@@ -6257,11 +7224,11 @@ program2.command("smoke <url>").description("Run autonomous smoke test").option(
|
|
|
6257
7224
|
const hasCritical = smokeResult.issuesFound.some((i) => i.severity === "critical" || i.severity === "high");
|
|
6258
7225
|
process.exit(hasCritical ? 1 : 0);
|
|
6259
7226
|
} catch (error) {
|
|
6260
|
-
console.error(
|
|
7227
|
+
console.error(chalk5.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
6261
7228
|
process.exit(1);
|
|
6262
7229
|
}
|
|
6263
7230
|
});
|
|
6264
|
-
program2.command("diff <run1> <run2>").description("Compare two test runs").option("--json", "JSON output", false).action((run1, run2, opts) => {
|
|
7231
|
+
program2.command("diff <run1> <run2>").description("Compare two test runs").option("--json", "JSON output", false).option("--threshold <percent>", "Visual diff threshold percentage", "0.1").action((run1, run2, opts) => {
|
|
6265
7232
|
try {
|
|
6266
7233
|
const diff = diffRuns(run1, run2);
|
|
6267
7234
|
if (opts.json) {
|
|
@@ -6269,9 +7236,19 @@ program2.command("diff <run1> <run2>").description("Compare two test runs").opti
|
|
|
6269
7236
|
} else {
|
|
6270
7237
|
console.log(formatDiffTerminal(diff));
|
|
6271
7238
|
}
|
|
6272
|
-
|
|
7239
|
+
const threshold = parseFloat(opts.threshold);
|
|
7240
|
+
const visualResults = compareRunScreenshots(run2, run1, threshold);
|
|
7241
|
+
if (visualResults.length > 0) {
|
|
7242
|
+
if (opts.json) {
|
|
7243
|
+
console.log(JSON.stringify({ visualDiff: visualResults }, null, 2));
|
|
7244
|
+
} else {
|
|
7245
|
+
console.log(formatVisualDiffTerminal(visualResults, threshold));
|
|
7246
|
+
}
|
|
7247
|
+
}
|
|
7248
|
+
const hasVisualRegressions = visualResults.some((r) => r.isRegression);
|
|
7249
|
+
process.exit(diff.regressions.length > 0 || hasVisualRegressions ? 1 : 0);
|
|
6273
7250
|
} catch (error) {
|
|
6274
|
-
console.error(
|
|
7251
|
+
console.error(chalk5.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
6275
7252
|
process.exit(1);
|
|
6276
7253
|
}
|
|
6277
7254
|
});
|
|
@@ -6284,9 +7261,9 @@ program2.command("report [run-id]").description("Generate HTML test report").opt
|
|
|
6284
7261
|
html = generateHtmlReport(runId);
|
|
6285
7262
|
}
|
|
6286
7263
|
writeFileSync3(opts.output, html, "utf-8");
|
|
6287
|
-
console.log(
|
|
7264
|
+
console.log(chalk5.green(`Report generated: ${opts.output}`));
|
|
6288
7265
|
} catch (error) {
|
|
6289
|
-
console.error(
|
|
7266
|
+
console.error(chalk5.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
6290
7267
|
process.exit(1);
|
|
6291
7268
|
}
|
|
6292
7269
|
});
|
|
@@ -6299,9 +7276,9 @@ authCmd.command("add <name>").description("Create an auth preset").requiredOptio
|
|
|
6299
7276
|
password: opts.password,
|
|
6300
7277
|
loginPath: opts.loginPath
|
|
6301
7278
|
});
|
|
6302
|
-
console.log(
|
|
7279
|
+
console.log(chalk5.green(`Created auth preset ${chalk5.bold(preset.name)} (${preset.email})`));
|
|
6303
7280
|
} catch (error) {
|
|
6304
|
-
console.error(
|
|
7281
|
+
console.error(chalk5.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
6305
7282
|
process.exit(1);
|
|
6306
7283
|
}
|
|
6307
7284
|
});
|
|
@@ -6309,11 +7286,11 @@ authCmd.command("list").description("List auth presets").action(() => {
|
|
|
6309
7286
|
try {
|
|
6310
7287
|
const presets = listAuthPresets();
|
|
6311
7288
|
if (presets.length === 0) {
|
|
6312
|
-
console.log(
|
|
7289
|
+
console.log(chalk5.dim("No auth presets found."));
|
|
6313
7290
|
return;
|
|
6314
7291
|
}
|
|
6315
7292
|
console.log("");
|
|
6316
|
-
console.log(
|
|
7293
|
+
console.log(chalk5.bold(" Auth Presets"));
|
|
6317
7294
|
console.log("");
|
|
6318
7295
|
console.log(` ${"Name".padEnd(20)} ${"Email".padEnd(30)} ${"Login Path".padEnd(15)} Created`);
|
|
6319
7296
|
console.log(` ${"\u2500".repeat(20)} ${"\u2500".repeat(30)} ${"\u2500".repeat(15)} ${"\u2500".repeat(22)}`);
|
|
@@ -6322,7 +7299,7 @@ authCmd.command("list").description("List auth presets").action(() => {
|
|
|
6322
7299
|
}
|
|
6323
7300
|
console.log("");
|
|
6324
7301
|
} catch (error) {
|
|
6325
|
-
console.error(
|
|
7302
|
+
console.error(chalk5.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
6326
7303
|
process.exit(1);
|
|
6327
7304
|
}
|
|
6328
7305
|
});
|
|
@@ -6330,13 +7307,13 @@ authCmd.command("delete <name>").description("Delete an auth preset").action((na
|
|
|
6330
7307
|
try {
|
|
6331
7308
|
const deleted = deleteAuthPreset(name);
|
|
6332
7309
|
if (deleted) {
|
|
6333
|
-
console.log(
|
|
7310
|
+
console.log(chalk5.green(`Deleted auth preset: ${name}`));
|
|
6334
7311
|
} else {
|
|
6335
|
-
console.error(
|
|
7312
|
+
console.error(chalk5.red(`Auth preset not found: ${name}`));
|
|
6336
7313
|
process.exit(1);
|
|
6337
7314
|
}
|
|
6338
7315
|
} catch (error) {
|
|
6339
|
-
console.error(
|
|
7316
|
+
console.error(chalk5.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
6340
7317
|
process.exit(1);
|
|
6341
7318
|
}
|
|
6342
7319
|
});
|
|
@@ -6349,7 +7326,268 @@ program2.command("costs").description("Show cost tracking and budget status").op
|
|
|
6349
7326
|
console.log(formatCostsTerminal(summary));
|
|
6350
7327
|
}
|
|
6351
7328
|
} catch (error) {
|
|
6352
|
-
console.error(
|
|
7329
|
+
console.error(chalk5.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
7330
|
+
process.exit(1);
|
|
7331
|
+
}
|
|
7332
|
+
});
|
|
7333
|
+
program2.command("chain <scenario-id>").description("Add a dependency to a scenario").requiredOption("--depends-on <id>", "Scenario ID that must run first").action((scenarioId, opts) => {
|
|
7334
|
+
try {
|
|
7335
|
+
const scenario = getScenario(scenarioId) ?? getScenarioByShortId(scenarioId);
|
|
7336
|
+
if (!scenario) {
|
|
7337
|
+
console.error(chalk5.red(`Scenario not found: ${scenarioId}`));
|
|
7338
|
+
process.exit(1);
|
|
7339
|
+
}
|
|
7340
|
+
const dep = getScenario(opts.dependsOn) ?? getScenarioByShortId(opts.dependsOn);
|
|
7341
|
+
if (!dep) {
|
|
7342
|
+
console.error(chalk5.red(`Dependency scenario not found: ${opts.dependsOn}`));
|
|
7343
|
+
process.exit(1);
|
|
7344
|
+
}
|
|
7345
|
+
addDependency(scenario.id, dep.id);
|
|
7346
|
+
console.log(chalk5.green(`${scenario.shortId} now depends on ${dep.shortId}`));
|
|
7347
|
+
} catch (error) {
|
|
7348
|
+
console.error(chalk5.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
7349
|
+
process.exit(1);
|
|
7350
|
+
}
|
|
7351
|
+
});
|
|
7352
|
+
program2.command("unchain <scenario-id>").description("Remove a dependency from a scenario").requiredOption("--from <id>", "Dependency to remove").action((scenarioId, opts) => {
|
|
7353
|
+
try {
|
|
7354
|
+
const scenario = getScenario(scenarioId) ?? getScenarioByShortId(scenarioId);
|
|
7355
|
+
if (!scenario) {
|
|
7356
|
+
console.error(chalk5.red(`Scenario not found: ${scenarioId}`));
|
|
7357
|
+
process.exit(1);
|
|
7358
|
+
}
|
|
7359
|
+
const dep = getScenario(opts.from) ?? getScenarioByShortId(opts.from);
|
|
7360
|
+
if (!dep) {
|
|
7361
|
+
console.error(chalk5.red(`Dependency not found: ${opts.from}`));
|
|
7362
|
+
process.exit(1);
|
|
7363
|
+
}
|
|
7364
|
+
removeDependency(scenario.id, dep.id);
|
|
7365
|
+
console.log(chalk5.green(`Removed dependency: ${scenario.shortId} no longer depends on ${dep.shortId}`));
|
|
7366
|
+
} catch (error) {
|
|
7367
|
+
console.error(chalk5.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
7368
|
+
process.exit(1);
|
|
7369
|
+
}
|
|
7370
|
+
});
|
|
7371
|
+
program2.command("deps <scenario-id>").description("Show dependencies for a scenario").action((scenarioId) => {
|
|
7372
|
+
try {
|
|
7373
|
+
const scenario = getScenario(scenarioId) ?? getScenarioByShortId(scenarioId);
|
|
7374
|
+
if (!scenario) {
|
|
7375
|
+
console.error(chalk5.red(`Scenario not found: ${scenarioId}`));
|
|
7376
|
+
process.exit(1);
|
|
7377
|
+
}
|
|
7378
|
+
const deps = getDependencies(scenario.id);
|
|
7379
|
+
const dependents = getDependents(scenario.id);
|
|
7380
|
+
console.log("");
|
|
7381
|
+
console.log(chalk5.bold(` Dependencies for ${scenario.shortId}: ${scenario.name}`));
|
|
7382
|
+
console.log("");
|
|
7383
|
+
if (deps.length > 0) {
|
|
7384
|
+
console.log(chalk5.dim(" Depends on:"));
|
|
7385
|
+
for (const depId of deps) {
|
|
7386
|
+
const s = getScenario(depId);
|
|
7387
|
+
console.log(` \u2192 ${s ? `${s.shortId}: ${s.name}` : depId.slice(0, 8)}`);
|
|
7388
|
+
}
|
|
7389
|
+
} else {
|
|
7390
|
+
console.log(chalk5.dim(" No dependencies"));
|
|
7391
|
+
}
|
|
7392
|
+
if (dependents.length > 0) {
|
|
7393
|
+
console.log("");
|
|
7394
|
+
console.log(chalk5.dim(" Required by:"));
|
|
7395
|
+
for (const depId of dependents) {
|
|
7396
|
+
const s = getScenario(depId);
|
|
7397
|
+
console.log(` \u2190 ${s ? `${s.shortId}: ${s.name}` : depId.slice(0, 8)}`);
|
|
7398
|
+
}
|
|
7399
|
+
}
|
|
7400
|
+
console.log("");
|
|
7401
|
+
} catch (error) {
|
|
7402
|
+
console.error(chalk5.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
7403
|
+
process.exit(1);
|
|
7404
|
+
}
|
|
7405
|
+
});
|
|
7406
|
+
var flowCmd = program2.command("flow").description("Manage test flows (ordered scenario chains)");
|
|
7407
|
+
flowCmd.command("create <name>").description("Create a flow from scenario IDs").requiredOption("--chain <ids>", "Comma-separated scenario IDs in order").option("--project <id>", "Project ID").action((name, opts) => {
|
|
7408
|
+
try {
|
|
7409
|
+
const ids = opts.chain.split(",").map((id) => {
|
|
7410
|
+
const s = getScenario(id.trim()) ?? getScenarioByShortId(id.trim());
|
|
7411
|
+
if (!s) {
|
|
7412
|
+
console.error(chalk5.red(`Scenario not found: ${id.trim()}`));
|
|
7413
|
+
process.exit(1);
|
|
7414
|
+
}
|
|
7415
|
+
return s.id;
|
|
7416
|
+
});
|
|
7417
|
+
for (let i = 1;i < ids.length; i++) {
|
|
7418
|
+
try {
|
|
7419
|
+
addDependency(ids[i], ids[i - 1]);
|
|
7420
|
+
} catch {}
|
|
7421
|
+
}
|
|
7422
|
+
const flow = createFlow({ name, scenarioIds: ids, projectId: resolveProject(opts.project) });
|
|
7423
|
+
console.log(chalk5.green(`Flow created: ${flow.id.slice(0, 8)} \u2014 ${flow.name} (${ids.length} scenarios)`));
|
|
7424
|
+
} catch (error) {
|
|
7425
|
+
console.error(chalk5.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
7426
|
+
process.exit(1);
|
|
7427
|
+
}
|
|
7428
|
+
});
|
|
7429
|
+
flowCmd.command("list").description("List all flows").option("--project <id>", "Project ID").action((opts) => {
|
|
7430
|
+
const flows = listFlows(resolveProject(opts.project) ?? undefined);
|
|
7431
|
+
if (flows.length === 0) {
|
|
7432
|
+
console.log(chalk5.dim(`
|
|
7433
|
+
No flows found.
|
|
7434
|
+
`));
|
|
7435
|
+
return;
|
|
7436
|
+
}
|
|
7437
|
+
console.log("");
|
|
7438
|
+
console.log(chalk5.bold(" Flows"));
|
|
7439
|
+
console.log("");
|
|
7440
|
+
for (const f of flows) {
|
|
7441
|
+
console.log(` ${chalk5.dim(f.id.slice(0, 8))} ${f.name} ${chalk5.dim(`(${f.scenarioIds.length} scenarios)`)}`);
|
|
7442
|
+
}
|
|
7443
|
+
console.log("");
|
|
7444
|
+
});
|
|
7445
|
+
flowCmd.command("show <id>").description("Show flow details").action((id) => {
|
|
7446
|
+
const flow = getFlow(id);
|
|
7447
|
+
if (!flow) {
|
|
7448
|
+
console.error(chalk5.red(`Flow not found: ${id}`));
|
|
7449
|
+
process.exit(1);
|
|
7450
|
+
}
|
|
7451
|
+
console.log("");
|
|
7452
|
+
console.log(chalk5.bold(` Flow: ${flow.name}`));
|
|
7453
|
+
console.log(` ID: ${chalk5.dim(flow.id)}`);
|
|
7454
|
+
console.log(` Scenarios (in order):`);
|
|
7455
|
+
for (let i = 0;i < flow.scenarioIds.length; i++) {
|
|
7456
|
+
const s = getScenario(flow.scenarioIds[i]);
|
|
7457
|
+
console.log(` ${i + 1}. ${s ? `${s.shortId}: ${s.name}` : flow.scenarioIds[i].slice(0, 8)}`);
|
|
7458
|
+
}
|
|
7459
|
+
console.log("");
|
|
7460
|
+
});
|
|
7461
|
+
flowCmd.command("delete <id>").description("Delete a flow").action((id) => {
|
|
7462
|
+
if (deleteFlow(id))
|
|
7463
|
+
console.log(chalk5.green("Flow deleted."));
|
|
7464
|
+
else {
|
|
7465
|
+
console.error(chalk5.red("Flow not found."));
|
|
7466
|
+
process.exit(1);
|
|
7467
|
+
}
|
|
7468
|
+
});
|
|
7469
|
+
flowCmd.command("run <id>").description("Run a flow (scenarios in dependency order)").option("-u, --url <url>", "Target URL (required)").option("-m, --model <model>", "AI model").option("--headed", "Run headed", false).option("--json", "JSON output", false).action(async (id, opts) => {
|
|
7470
|
+
try {
|
|
7471
|
+
const flow = getFlow(id);
|
|
7472
|
+
if (!flow) {
|
|
7473
|
+
console.error(chalk5.red(`Flow not found: ${id}`));
|
|
7474
|
+
process.exit(1);
|
|
7475
|
+
}
|
|
7476
|
+
if (!opts.url) {
|
|
7477
|
+
console.error(chalk5.red("--url is required for flow run"));
|
|
7478
|
+
process.exit(1);
|
|
7479
|
+
}
|
|
7480
|
+
console.log(chalk5.blue(`Running flow: ${flow.name} (${flow.scenarioIds.length} scenarios)`));
|
|
7481
|
+
const { run, results } = await runByFilter({
|
|
7482
|
+
url: opts.url,
|
|
7483
|
+
scenarioIds: flow.scenarioIds,
|
|
7484
|
+
model: opts.model,
|
|
7485
|
+
headed: opts.headed,
|
|
7486
|
+
parallel: 1
|
|
7487
|
+
});
|
|
7488
|
+
if (opts.json)
|
|
7489
|
+
console.log(formatJSON(run, results));
|
|
7490
|
+
else
|
|
7491
|
+
console.log(formatTerminal(run, results));
|
|
7492
|
+
process.exit(getExitCode(run));
|
|
7493
|
+
} catch (error) {
|
|
7494
|
+
console.error(chalk5.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
7495
|
+
process.exit(1);
|
|
7496
|
+
}
|
|
7497
|
+
});
|
|
7498
|
+
var envCmd = program2.command("env").description("Manage environments");
|
|
7499
|
+
envCmd.command("add <name>").description("Add a named environment").requiredOption("--url <url>", "Environment URL").option("--auth <preset>", "Auth preset name").option("--project <id>", "Project ID").option("--default", "Set as default environment", false).action((name, opts) => {
|
|
7500
|
+
try {
|
|
7501
|
+
const env = createEnvironment({
|
|
7502
|
+
name,
|
|
7503
|
+
url: opts.url,
|
|
7504
|
+
authPresetName: opts.auth,
|
|
7505
|
+
projectId: opts.project,
|
|
7506
|
+
isDefault: opts.default
|
|
7507
|
+
});
|
|
7508
|
+
console.log(chalk5.green(`Environment added: ${env.name} \u2192 ${env.url}${env.isDefault ? " (default)" : ""}`));
|
|
7509
|
+
} catch (error) {
|
|
7510
|
+
console.error(chalk5.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
7511
|
+
process.exit(1);
|
|
7512
|
+
}
|
|
7513
|
+
});
|
|
7514
|
+
envCmd.command("list").description("List all environments").option("--project <id>", "Filter by project ID").action((opts) => {
|
|
7515
|
+
try {
|
|
7516
|
+
const envs = listEnvironments(opts.project);
|
|
7517
|
+
if (envs.length === 0) {
|
|
7518
|
+
console.log(chalk5.dim("No environments configured. Add one with: testers env add <name> --url <url>"));
|
|
7519
|
+
return;
|
|
7520
|
+
}
|
|
7521
|
+
for (const env of envs) {
|
|
7522
|
+
const marker = env.isDefault ? chalk5.green(" \u2605 default") : "";
|
|
7523
|
+
const auth = env.authPresetName ? chalk5.dim(` (auth: ${env.authPresetName})`) : "";
|
|
7524
|
+
console.log(` ${chalk5.bold(env.name)} ${env.url}${auth}${marker}`);
|
|
7525
|
+
}
|
|
7526
|
+
} catch (error) {
|
|
7527
|
+
console.error(chalk5.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
7528
|
+
process.exit(1);
|
|
7529
|
+
}
|
|
7530
|
+
});
|
|
7531
|
+
envCmd.command("use <name>").description("Set an environment as the default").action((name) => {
|
|
7532
|
+
try {
|
|
7533
|
+
setDefaultEnvironment(name);
|
|
7534
|
+
const env = getEnvironment(name);
|
|
7535
|
+
console.log(chalk5.green(`Default environment set: ${env.name} \u2192 ${env.url}`));
|
|
7536
|
+
} catch (error) {
|
|
7537
|
+
console.error(chalk5.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
7538
|
+
process.exit(1);
|
|
7539
|
+
}
|
|
7540
|
+
});
|
|
7541
|
+
envCmd.command("delete <name>").description("Delete an environment").action((name) => {
|
|
7542
|
+
try {
|
|
7543
|
+
const deleted = deleteEnvironment(name);
|
|
7544
|
+
if (deleted) {
|
|
7545
|
+
console.log(chalk5.green(`Environment deleted: ${name}`));
|
|
7546
|
+
} else {
|
|
7547
|
+
console.error(chalk5.red(`Environment not found: ${name}`));
|
|
7548
|
+
process.exit(1);
|
|
7549
|
+
}
|
|
7550
|
+
} catch (error) {
|
|
7551
|
+
console.error(chalk5.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
7552
|
+
process.exit(1);
|
|
7553
|
+
}
|
|
7554
|
+
});
|
|
7555
|
+
program2.command("baseline <run-id>").description("Set a run as the visual baseline").action((runId) => {
|
|
7556
|
+
try {
|
|
7557
|
+
setBaseline(runId);
|
|
7558
|
+
const run = getRun(runId);
|
|
7559
|
+
console.log(chalk5.green(`Baseline set: ${chalk5.bold(runId.slice(0, 8))}${run ? ` (${run.status}, ${run.total} scenarios)` : ""}`));
|
|
7560
|
+
} catch (error) {
|
|
7561
|
+
console.error(chalk5.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
7562
|
+
process.exit(1);
|
|
7563
|
+
}
|
|
7564
|
+
});
|
|
7565
|
+
program2.command("import-api <spec>").description("Import test scenarios from an OpenAPI/Swagger spec file").option("--project <id>", "Project ID").action(async (spec, opts) => {
|
|
7566
|
+
try {
|
|
7567
|
+
const { importFromOpenAPI: importFromOpenAPI2 } = await Promise.resolve().then(() => (init_openapi_import(), exports_openapi_import));
|
|
7568
|
+
const { imported, scenarios } = importFromOpenAPI2(spec, resolveProject(opts.project) ?? undefined);
|
|
7569
|
+
console.log(chalk5.green(`
|
|
7570
|
+
Imported ${imported} scenarios from API spec:`));
|
|
7571
|
+
for (const s of scenarios) {
|
|
7572
|
+
console.log(` ${chalk5.cyan(s.shortId)} ${s.name} ${chalk5.dim(`[${s.tags.join(", ")}]`)}`);
|
|
7573
|
+
}
|
|
7574
|
+
console.log("");
|
|
7575
|
+
} catch (error) {
|
|
7576
|
+
console.error(chalk5.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
7577
|
+
process.exit(1);
|
|
7578
|
+
}
|
|
7579
|
+
});
|
|
7580
|
+
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) => {
|
|
7581
|
+
try {
|
|
7582
|
+
const { recordAndSave: recordAndSave2 } = await Promise.resolve().then(() => (init_recorder(), exports_recorder));
|
|
7583
|
+
console.log(chalk5.blue("Opening browser for recording..."));
|
|
7584
|
+
const { recording, scenario } = await recordAndSave2(url, opts.name, resolveProject(opts.project) ?? undefined);
|
|
7585
|
+
console.log("");
|
|
7586
|
+
console.log(chalk5.green(`Recording saved as scenario ${chalk5.bold(scenario.shortId)}: ${scenario.name}`));
|
|
7587
|
+
console.log(chalk5.dim(` ${recording.actions.length} actions recorded in ${(recording.duration / 1000).toFixed(0)}s`));
|
|
7588
|
+
console.log(chalk5.dim(` ${scenario.steps.length} steps generated`));
|
|
7589
|
+
} catch (error) {
|
|
7590
|
+
console.error(chalk5.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
6353
7591
|
process.exit(1);
|
|
6354
7592
|
}
|
|
6355
7593
|
});
|