@hasna/testers 0.0.12 → 0.0.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dashboard/dist/assets/{index-DyXKnBM8.css → index-PT-52SEY.css} +1 -1
- package/dashboard/dist/index.html +2 -2
- package/dist/cli/index.js +2110 -1469
- package/dist/db/agents.d.ts +2 -0
- package/dist/db/agents.d.ts.map +1 -1
- package/dist/index.js +215 -117
- package/dist/lib/affected.d.ts +23 -0
- package/dist/lib/affected.d.ts.map +1 -0
- package/dist/lib/failure-pipeline.d.ts +20 -0
- package/dist/lib/failure-pipeline.d.ts.map +1 -0
- package/dist/lib/git-watch.d.ts +16 -0
- package/dist/lib/git-watch.d.ts.map +1 -0
- package/dist/lib/runner.d.ts.map +1 -1
- package/dist/mcp/index.js +314 -94
- package/dist/server/index.js +138 -13
- package/package.json +1 -1
- package/dist/cli/index.d.ts +0 -3
- package/dist/cli/index.d.ts.map +0 -1
- package/dist/mcp/index.d.ts +0 -3
- package/dist/mcp/index.d.ts.map +0 -1
- /package/dashboard/dist/assets/{index-jNG_Nd_Q.js → index-FZ9gzLaz.js} +0 -0
package/dist/cli/index.js
CHANGED
|
@@ -4,26 +4,62 @@ var __create = Object.create;
|
|
|
4
4
|
var __getProtoOf = Object.getPrototypeOf;
|
|
5
5
|
var __defProp = Object.defineProperty;
|
|
6
6
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
7
8
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
+
function __accessProp(key) {
|
|
10
|
+
return this[key];
|
|
11
|
+
}
|
|
12
|
+
var __toESMCache_node;
|
|
13
|
+
var __toESMCache_esm;
|
|
8
14
|
var __toESM = (mod, isNodeMode, target) => {
|
|
15
|
+
var canCache = mod != null && typeof mod === "object";
|
|
16
|
+
if (canCache) {
|
|
17
|
+
var cache = isNodeMode ? __toESMCache_node ??= new WeakMap : __toESMCache_esm ??= new WeakMap;
|
|
18
|
+
var cached = cache.get(mod);
|
|
19
|
+
if (cached)
|
|
20
|
+
return cached;
|
|
21
|
+
}
|
|
9
22
|
target = mod != null ? __create(__getProtoOf(mod)) : {};
|
|
10
23
|
const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
|
|
11
24
|
for (let key of __getOwnPropNames(mod))
|
|
12
25
|
if (!__hasOwnProp.call(to, key))
|
|
13
26
|
__defProp(to, key, {
|
|
14
|
-
get: (
|
|
27
|
+
get: __accessProp.bind(mod, key),
|
|
15
28
|
enumerable: true
|
|
16
29
|
});
|
|
30
|
+
if (canCache)
|
|
31
|
+
cache.set(mod, to);
|
|
17
32
|
return to;
|
|
18
33
|
};
|
|
34
|
+
var __toCommonJS = (from) => {
|
|
35
|
+
var entry = (__moduleCache ??= new WeakMap).get(from), desc;
|
|
36
|
+
if (entry)
|
|
37
|
+
return entry;
|
|
38
|
+
entry = __defProp({}, "__esModule", { value: true });
|
|
39
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
40
|
+
for (var key of __getOwnPropNames(from))
|
|
41
|
+
if (!__hasOwnProp.call(entry, key))
|
|
42
|
+
__defProp(entry, key, {
|
|
43
|
+
get: __accessProp.bind(from, key),
|
|
44
|
+
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
__moduleCache.set(from, entry);
|
|
48
|
+
return entry;
|
|
49
|
+
};
|
|
50
|
+
var __moduleCache;
|
|
19
51
|
var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
|
|
52
|
+
var __returnValue = (v) => v;
|
|
53
|
+
function __exportSetter(name, newValue) {
|
|
54
|
+
this[name] = __returnValue.bind(null, newValue);
|
|
55
|
+
}
|
|
20
56
|
var __export = (target, all) => {
|
|
21
57
|
for (var name in all)
|
|
22
58
|
__defProp(target, name, {
|
|
23
59
|
get: all[name],
|
|
24
60
|
enumerable: true,
|
|
25
61
|
configurable: true,
|
|
26
|
-
set: (
|
|
62
|
+
set: __exportSetter.bind(all, name)
|
|
27
63
|
});
|
|
28
64
|
};
|
|
29
65
|
var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
@@ -2074,6 +2110,17 @@ function projectFromRow(row) {
|
|
|
2074
2110
|
updatedAt: row.updated_at
|
|
2075
2111
|
};
|
|
2076
2112
|
}
|
|
2113
|
+
function agentFromRow(row) {
|
|
2114
|
+
return {
|
|
2115
|
+
id: row.id,
|
|
2116
|
+
name: row.name,
|
|
2117
|
+
description: row.description,
|
|
2118
|
+
role: row.role,
|
|
2119
|
+
metadata: row.metadata ? JSON.parse(row.metadata) : null,
|
|
2120
|
+
createdAt: row.created_at,
|
|
2121
|
+
lastSeenAt: row.last_seen_at
|
|
2122
|
+
};
|
|
2123
|
+
}
|
|
2077
2124
|
function scenarioFromRow(row) {
|
|
2078
2125
|
return {
|
|
2079
2126
|
id: row.id,
|
|
@@ -2820,6 +2867,112 @@ var init_runs = __esm(() => {
|
|
|
2820
2867
|
init_database();
|
|
2821
2868
|
});
|
|
2822
2869
|
|
|
2870
|
+
// src/db/results.ts
|
|
2871
|
+
function createResult(input) {
|
|
2872
|
+
const db2 = getDatabase();
|
|
2873
|
+
const id = uuid();
|
|
2874
|
+
const timestamp = now();
|
|
2875
|
+
db2.query(`
|
|
2876
|
+
INSERT INTO results (id, run_id, scenario_id, status, reasoning, error, steps_completed, steps_total, duration_ms, model, tokens_used, cost_cents, metadata, created_at)
|
|
2877
|
+
VALUES (?, ?, ?, 'skipped', NULL, NULL, 0, ?, 0, ?, 0, 0, '{}', ?)
|
|
2878
|
+
`).run(id, input.runId, input.scenarioId, input.stepsTotal, input.model, timestamp);
|
|
2879
|
+
return getResult(id);
|
|
2880
|
+
}
|
|
2881
|
+
function getResult(id) {
|
|
2882
|
+
const db2 = getDatabase();
|
|
2883
|
+
let row = db2.query("SELECT * FROM results WHERE id = ?").get(id);
|
|
2884
|
+
if (row)
|
|
2885
|
+
return resultFromRow(row);
|
|
2886
|
+
const fullId = resolvePartialId("results", id);
|
|
2887
|
+
if (fullId) {
|
|
2888
|
+
row = db2.query("SELECT * FROM results WHERE id = ?").get(fullId);
|
|
2889
|
+
if (row)
|
|
2890
|
+
return resultFromRow(row);
|
|
2891
|
+
}
|
|
2892
|
+
return null;
|
|
2893
|
+
}
|
|
2894
|
+
function listResults(runId) {
|
|
2895
|
+
const db2 = getDatabase();
|
|
2896
|
+
const rows = db2.query("SELECT * FROM results WHERE run_id = ? ORDER BY created_at ASC").all(runId);
|
|
2897
|
+
return rows.map(resultFromRow);
|
|
2898
|
+
}
|
|
2899
|
+
function updateResult(id, updates) {
|
|
2900
|
+
const db2 = getDatabase();
|
|
2901
|
+
const existing = getResult(id);
|
|
2902
|
+
if (!existing) {
|
|
2903
|
+
throw new Error(`Result not found: ${id}`);
|
|
2904
|
+
}
|
|
2905
|
+
const sets = [];
|
|
2906
|
+
const params = [];
|
|
2907
|
+
if (updates.status !== undefined) {
|
|
2908
|
+
sets.push("status = ?");
|
|
2909
|
+
params.push(updates.status);
|
|
2910
|
+
}
|
|
2911
|
+
if (updates.reasoning !== undefined) {
|
|
2912
|
+
sets.push("reasoning = ?");
|
|
2913
|
+
params.push(updates.reasoning);
|
|
2914
|
+
}
|
|
2915
|
+
if (updates.error !== undefined) {
|
|
2916
|
+
sets.push("error = ?");
|
|
2917
|
+
params.push(updates.error);
|
|
2918
|
+
}
|
|
2919
|
+
if (updates.stepsCompleted !== undefined) {
|
|
2920
|
+
sets.push("steps_completed = ?");
|
|
2921
|
+
params.push(updates.stepsCompleted);
|
|
2922
|
+
}
|
|
2923
|
+
if (updates.durationMs !== undefined) {
|
|
2924
|
+
sets.push("duration_ms = ?");
|
|
2925
|
+
params.push(updates.durationMs);
|
|
2926
|
+
}
|
|
2927
|
+
if (updates.tokensUsed !== undefined) {
|
|
2928
|
+
sets.push("tokens_used = ?");
|
|
2929
|
+
params.push(updates.tokensUsed);
|
|
2930
|
+
}
|
|
2931
|
+
if (updates.costCents !== undefined) {
|
|
2932
|
+
sets.push("cost_cents = ?");
|
|
2933
|
+
params.push(updates.costCents);
|
|
2934
|
+
}
|
|
2935
|
+
if (sets.length === 0) {
|
|
2936
|
+
return existing;
|
|
2937
|
+
}
|
|
2938
|
+
params.push(existing.id);
|
|
2939
|
+
db2.query(`UPDATE results SET ${sets.join(", ")} WHERE id = ?`).run(...params);
|
|
2940
|
+
return getResult(existing.id);
|
|
2941
|
+
}
|
|
2942
|
+
function getResultsByRun(runId) {
|
|
2943
|
+
return listResults(runId);
|
|
2944
|
+
}
|
|
2945
|
+
var init_results = __esm(() => {
|
|
2946
|
+
init_types();
|
|
2947
|
+
init_database();
|
|
2948
|
+
});
|
|
2949
|
+
|
|
2950
|
+
// src/db/screenshots.ts
|
|
2951
|
+
function createScreenshot(input) {
|
|
2952
|
+
const db2 = getDatabase();
|
|
2953
|
+
const id = uuid();
|
|
2954
|
+
const timestamp = now();
|
|
2955
|
+
db2.query(`
|
|
2956
|
+
INSERT INTO screenshots (id, result_id, step_number, action, file_path, width, height, timestamp, description, page_url, thumbnail_path)
|
|
2957
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
2958
|
+
`).run(id, input.resultId, input.stepNumber, input.action, input.filePath, input.width, input.height, timestamp, input.description ?? null, input.pageUrl ?? null, input.thumbnailPath ?? null);
|
|
2959
|
+
return getScreenshot(id);
|
|
2960
|
+
}
|
|
2961
|
+
function getScreenshot(id) {
|
|
2962
|
+
const db2 = getDatabase();
|
|
2963
|
+
const row = db2.query("SELECT * FROM screenshots WHERE id = ?").get(id);
|
|
2964
|
+
return row ? screenshotFromRow(row) : null;
|
|
2965
|
+
}
|
|
2966
|
+
function listScreenshots(resultId) {
|
|
2967
|
+
const db2 = getDatabase();
|
|
2968
|
+
const rows = db2.query("SELECT * FROM screenshots WHERE result_id = ? ORDER BY step_number ASC").all(resultId);
|
|
2969
|
+
return rows.map(screenshotFromRow);
|
|
2970
|
+
}
|
|
2971
|
+
var init_screenshots = __esm(() => {
|
|
2972
|
+
init_types();
|
|
2973
|
+
init_database();
|
|
2974
|
+
});
|
|
2975
|
+
|
|
2823
2976
|
// src/lib/browser-lightpanda.ts
|
|
2824
2977
|
var exports_browser_lightpanda = {};
|
|
2825
2978
|
__export(exports_browser_lightpanda, {
|
|
@@ -2982,694 +3135,69 @@ var init_browser_lightpanda = __esm(() => {
|
|
|
2982
3135
|
init_types();
|
|
2983
3136
|
});
|
|
2984
3137
|
|
|
2985
|
-
// src/
|
|
2986
|
-
|
|
2987
|
-
|
|
2988
|
-
|
|
2989
|
-
|
|
2990
|
-
|
|
2991
|
-
|
|
2992
|
-
|
|
2993
|
-
|
|
2994
|
-
getDependencies: () => getDependencies,
|
|
2995
|
-
deleteFlow: () => deleteFlow,
|
|
2996
|
-
createFlow: () => createFlow,
|
|
2997
|
-
addDependency: () => addDependency
|
|
2998
|
-
});
|
|
2999
|
-
function addDependency(scenarioId, dependsOn) {
|
|
3000
|
-
const db2 = getDatabase();
|
|
3001
|
-
const visited = new Set;
|
|
3002
|
-
const queue = [dependsOn];
|
|
3003
|
-
while (queue.length > 0) {
|
|
3004
|
-
const current = queue.shift();
|
|
3005
|
-
if (current === scenarioId) {
|
|
3006
|
-
throw new DependencyCycleError(scenarioId, dependsOn);
|
|
3007
|
-
}
|
|
3008
|
-
if (visited.has(current))
|
|
3009
|
-
continue;
|
|
3010
|
-
visited.add(current);
|
|
3011
|
-
const deps = db2.query("SELECT depends_on FROM scenario_dependencies WHERE scenario_id = ?").all(current);
|
|
3012
|
-
for (const dep of deps) {
|
|
3013
|
-
if (!visited.has(dep.depends_on)) {
|
|
3014
|
-
queue.push(dep.depends_on);
|
|
3015
|
-
}
|
|
3138
|
+
// src/lib/browser.ts
|
|
3139
|
+
import { chromium as chromium2 } from "playwright";
|
|
3140
|
+
import { execSync } from "child_process";
|
|
3141
|
+
async function launchBrowser(options) {
|
|
3142
|
+
const engine = options?.engine ?? process.env["TESTERS_BROWSER_ENGINE"] ?? "playwright";
|
|
3143
|
+
if (engine === "lightpanda") {
|
|
3144
|
+
const { launchLightpanda: launchLightpanda2, isLightpandaAvailable: isLightpandaAvailable2 } = await Promise.resolve().then(() => (init_browser_lightpanda(), exports_browser_lightpanda));
|
|
3145
|
+
if (!isLightpandaAvailable2()) {
|
|
3146
|
+
throw new BrowserError("Lightpanda not installed. Run: testers install-browser --engine lightpanda");
|
|
3016
3147
|
}
|
|
3148
|
+
return launchLightpanda2({ viewport: options?.viewport });
|
|
3149
|
+
}
|
|
3150
|
+
const headless = options?.headless ?? true;
|
|
3151
|
+
const viewport = options?.viewport ?? DEFAULT_VIEWPORT;
|
|
3152
|
+
try {
|
|
3153
|
+
const browser = await chromium2.launch({
|
|
3154
|
+
headless,
|
|
3155
|
+
args: [
|
|
3156
|
+
`--window-size=${viewport.width},${viewport.height}`
|
|
3157
|
+
]
|
|
3158
|
+
});
|
|
3159
|
+
return browser;
|
|
3160
|
+
} catch (error) {
|
|
3161
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3162
|
+
throw new BrowserError(`Failed to launch browser: ${message}`);
|
|
3017
3163
|
}
|
|
3018
|
-
db2.query("INSERT OR IGNORE INTO scenario_dependencies (scenario_id, depends_on) VALUES (?, ?)").run(scenarioId, dependsOn);
|
|
3019
|
-
}
|
|
3020
|
-
function removeDependency(scenarioId, dependsOn) {
|
|
3021
|
-
const db2 = getDatabase();
|
|
3022
|
-
const result = db2.query("DELETE FROM scenario_dependencies WHERE scenario_id = ? AND depends_on = ?").run(scenarioId, dependsOn);
|
|
3023
|
-
return result.changes > 0;
|
|
3024
|
-
}
|
|
3025
|
-
function getDependencies(scenarioId) {
|
|
3026
|
-
const db2 = getDatabase();
|
|
3027
|
-
const rows = db2.query("SELECT depends_on FROM scenario_dependencies WHERE scenario_id = ?").all(scenarioId);
|
|
3028
|
-
return rows.map((r) => r.depends_on);
|
|
3029
|
-
}
|
|
3030
|
-
function getDependents(scenarioId) {
|
|
3031
|
-
const db2 = getDatabase();
|
|
3032
|
-
const rows = db2.query("SELECT scenario_id FROM scenario_dependencies WHERE depends_on = ?").all(scenarioId);
|
|
3033
|
-
return rows.map((r) => r.scenario_id);
|
|
3034
3164
|
}
|
|
3035
|
-
function
|
|
3036
|
-
const
|
|
3037
|
-
|
|
3038
|
-
|
|
3039
|
-
|
|
3040
|
-
|
|
3041
|
-
|
|
3042
|
-
|
|
3043
|
-
|
|
3044
|
-
|
|
3045
|
-
|
|
3046
|
-
|
|
3047
|
-
}
|
|
3165
|
+
async function getPage(browser, options) {
|
|
3166
|
+
const engine = options?.engine ?? "playwright";
|
|
3167
|
+
if (engine === "lightpanda") {
|
|
3168
|
+
const { getLightpandaPage: getLightpandaPage2 } = await Promise.resolve().then(() => (init_browser_lightpanda(), exports_browser_lightpanda));
|
|
3169
|
+
return getLightpandaPage2(browser, options);
|
|
3170
|
+
}
|
|
3171
|
+
const viewport = options?.viewport ?? DEFAULT_VIEWPORT;
|
|
3172
|
+
try {
|
|
3173
|
+
const context = await browser.newContext({
|
|
3174
|
+
viewport,
|
|
3175
|
+
userAgent: options?.userAgent,
|
|
3176
|
+
locale: options?.locale
|
|
3177
|
+
});
|
|
3178
|
+
const page = await context.newPage();
|
|
3179
|
+
return page;
|
|
3180
|
+
} catch (error) {
|
|
3181
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3182
|
+
throw new BrowserError(`Failed to create page: ${message}`);
|
|
3048
3183
|
}
|
|
3049
|
-
return Array.from(visited);
|
|
3050
3184
|
}
|
|
3051
|
-
function
|
|
3052
|
-
|
|
3053
|
-
|
|
3054
|
-
|
|
3055
|
-
const dependents = new Map;
|
|
3056
|
-
for (const id of scenarioIds) {
|
|
3057
|
-
inDegree.set(id, 0);
|
|
3058
|
-
dependents.set(id, []);
|
|
3185
|
+
async function closeBrowser(browser, engine) {
|
|
3186
|
+
if (engine === "lightpanda") {
|
|
3187
|
+
const { closeLightpanda: closeLightpanda2 } = await Promise.resolve().then(() => (init_browser_lightpanda(), exports_browser_lightpanda));
|
|
3188
|
+
return closeLightpanda2(browser);
|
|
3059
3189
|
}
|
|
3060
|
-
|
|
3061
|
-
|
|
3062
|
-
|
|
3063
|
-
|
|
3064
|
-
|
|
3065
|
-
dependents.get(dep.depends_on).push(id);
|
|
3066
|
-
}
|
|
3067
|
-
}
|
|
3190
|
+
try {
|
|
3191
|
+
await browser.close();
|
|
3192
|
+
} catch (error) {
|
|
3193
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3194
|
+
throw new BrowserError(`Failed to close browser: ${message}`);
|
|
3068
3195
|
}
|
|
3069
|
-
|
|
3070
|
-
|
|
3071
|
-
|
|
3072
|
-
|
|
3073
|
-
|
|
3074
|
-
const sorted = [];
|
|
3075
|
-
while (queue.length > 0) {
|
|
3076
|
-
const current = queue.shift();
|
|
3077
|
-
sorted.push(current);
|
|
3078
|
-
for (const dep of dependents.get(current) ?? []) {
|
|
3079
|
-
const newDeg = (inDegree.get(dep) ?? 1) - 1;
|
|
3080
|
-
inDegree.set(dep, newDeg);
|
|
3081
|
-
if (newDeg === 0)
|
|
3082
|
-
queue.push(dep);
|
|
3083
|
-
}
|
|
3084
|
-
}
|
|
3085
|
-
if (sorted.length !== scenarioIds.length) {
|
|
3086
|
-
throw new DependencyCycleError("multiple", "multiple");
|
|
3087
|
-
}
|
|
3088
|
-
return sorted;
|
|
3089
|
-
}
|
|
3090
|
-
function createFlow(input) {
|
|
3091
|
-
const db2 = getDatabase();
|
|
3092
|
-
const id = uuid();
|
|
3093
|
-
const timestamp = now();
|
|
3094
|
-
db2.query(`
|
|
3095
|
-
INSERT INTO flows (id, project_id, name, description, scenario_ids, created_at, updated_at)
|
|
3096
|
-
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
3097
|
-
`).run(id, input.projectId ?? null, input.name, input.description ?? null, JSON.stringify(input.scenarioIds), timestamp, timestamp);
|
|
3098
|
-
return getFlow(id);
|
|
3099
|
-
}
|
|
3100
|
-
function getFlow(id) {
|
|
3101
|
-
const db2 = getDatabase();
|
|
3102
|
-
let row = db2.query("SELECT * FROM flows WHERE id = ?").get(id);
|
|
3103
|
-
if (row)
|
|
3104
|
-
return flowFromRow(row);
|
|
3105
|
-
const fullId = resolvePartialId("flows", id);
|
|
3106
|
-
if (fullId) {
|
|
3107
|
-
row = db2.query("SELECT * FROM flows WHERE id = ?").get(fullId);
|
|
3108
|
-
if (row)
|
|
3109
|
-
return flowFromRow(row);
|
|
3110
|
-
}
|
|
3111
|
-
return null;
|
|
3112
|
-
}
|
|
3113
|
-
function listFlows(projectId) {
|
|
3114
|
-
const db2 = getDatabase();
|
|
3115
|
-
if (projectId) {
|
|
3116
|
-
const rows2 = db2.query("SELECT * FROM flows WHERE project_id = ? ORDER BY created_at DESC").all(projectId);
|
|
3117
|
-
return rows2.map(flowFromRow);
|
|
3118
|
-
}
|
|
3119
|
-
const rows = db2.query("SELECT * FROM flows ORDER BY created_at DESC").all();
|
|
3120
|
-
return rows.map(flowFromRow);
|
|
3121
|
-
}
|
|
3122
|
-
function deleteFlow(id) {
|
|
3123
|
-
const db2 = getDatabase();
|
|
3124
|
-
const flow = getFlow(id);
|
|
3125
|
-
if (!flow)
|
|
3126
|
-
return false;
|
|
3127
|
-
const result = db2.query("DELETE FROM flows WHERE id = ?").run(flow.id);
|
|
3128
|
-
return result.changes > 0;
|
|
3129
|
-
}
|
|
3130
|
-
var init_flows = __esm(() => {
|
|
3131
|
-
init_database();
|
|
3132
|
-
init_database();
|
|
3133
|
-
init_types();
|
|
3134
|
-
});
|
|
3135
|
-
|
|
3136
|
-
// src/lib/openapi-import.ts
|
|
3137
|
-
var exports_openapi_import = {};
|
|
3138
|
-
__export(exports_openapi_import, {
|
|
3139
|
-
parseOpenAPISpec: () => parseOpenAPISpec,
|
|
3140
|
-
importFromOpenAPI: () => importFromOpenAPI
|
|
3141
|
-
});
|
|
3142
|
-
import { readFileSync as readFileSync5 } from "fs";
|
|
3143
|
-
function parseSpec(content) {
|
|
3144
|
-
try {
|
|
3145
|
-
return JSON.parse(content);
|
|
3146
|
-
} catch {
|
|
3147
|
-
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`");
|
|
3148
|
-
}
|
|
3149
|
-
}
|
|
3150
|
-
function methodPriority(method) {
|
|
3151
|
-
switch (method.toUpperCase()) {
|
|
3152
|
-
case "GET":
|
|
3153
|
-
return "medium";
|
|
3154
|
-
case "POST":
|
|
3155
|
-
return "high";
|
|
3156
|
-
case "PUT":
|
|
3157
|
-
return "high";
|
|
3158
|
-
case "DELETE":
|
|
3159
|
-
return "critical";
|
|
3160
|
-
case "PATCH":
|
|
3161
|
-
return "medium";
|
|
3162
|
-
default:
|
|
3163
|
-
return "low";
|
|
3164
|
-
}
|
|
3165
|
-
}
|
|
3166
|
-
function parseOpenAPISpec(filePathOrUrl) {
|
|
3167
|
-
let content;
|
|
3168
|
-
if (filePathOrUrl.startsWith("http")) {
|
|
3169
|
-
throw new Error("URL fetching not supported yet. Download the spec file first.");
|
|
3170
|
-
}
|
|
3171
|
-
content = readFileSync5(filePathOrUrl, "utf-8");
|
|
3172
|
-
const spec = parseSpec(content);
|
|
3173
|
-
const isOpenAPI3 = !!spec.openapi;
|
|
3174
|
-
const isSwagger2 = !!spec.swagger;
|
|
3175
|
-
if (!isOpenAPI3 && !isSwagger2) {
|
|
3176
|
-
throw new Error("Not a valid OpenAPI 3.x or Swagger 2.0 spec");
|
|
3177
|
-
}
|
|
3178
|
-
const scenarios = [];
|
|
3179
|
-
const paths = spec.paths ?? {};
|
|
3180
|
-
for (const [path, methods] of Object.entries(paths)) {
|
|
3181
|
-
for (const [method, operation] of Object.entries(methods)) {
|
|
3182
|
-
if (["get", "post", "put", "delete", "patch"].indexOf(method.toLowerCase()) === -1)
|
|
3183
|
-
continue;
|
|
3184
|
-
const op = operation;
|
|
3185
|
-
const name = op.summary ?? op.operationId ?? `${method.toUpperCase()} ${path}`;
|
|
3186
|
-
const tags = op.tags ?? [];
|
|
3187
|
-
const requiresAuth = !!(op.security?.length ?? spec.security?.length);
|
|
3188
|
-
const steps = [];
|
|
3189
|
-
steps.push(`Navigate to the API endpoint: ${method.toUpperCase()} ${path}`);
|
|
3190
|
-
if (op.parameters?.length) {
|
|
3191
|
-
const required = op.parameters.filter((p) => p.required);
|
|
3192
|
-
if (required.length > 0) {
|
|
3193
|
-
steps.push(`Fill required parameters: ${required.map((p) => p.name).join(", ")}`);
|
|
3194
|
-
}
|
|
3195
|
-
}
|
|
3196
|
-
if (["post", "put", "patch"].includes(method.toLowerCase())) {
|
|
3197
|
-
steps.push("Fill the request body with valid test data");
|
|
3198
|
-
}
|
|
3199
|
-
steps.push("Submit the request");
|
|
3200
|
-
const responses = op.responses ?? {};
|
|
3201
|
-
const successCodes = Object.keys(responses).filter((c) => c.startsWith("2"));
|
|
3202
|
-
if (successCodes.length > 0) {
|
|
3203
|
-
steps.push(`Verify response status is ${successCodes.join(" or ")}`);
|
|
3204
|
-
} else {
|
|
3205
|
-
steps.push("Verify the response is successful");
|
|
3206
|
-
}
|
|
3207
|
-
const description = [
|
|
3208
|
-
op.description ?? `Test the ${method.toUpperCase()} ${path} endpoint.`,
|
|
3209
|
-
requiresAuth ? "This endpoint requires authentication." : ""
|
|
3210
|
-
].filter(Boolean).join(" ");
|
|
3211
|
-
scenarios.push({
|
|
3212
|
-
name,
|
|
3213
|
-
description,
|
|
3214
|
-
steps,
|
|
3215
|
-
tags: [...tags, "api", method.toLowerCase()],
|
|
3216
|
-
priority: methodPriority(method),
|
|
3217
|
-
targetPath: path,
|
|
3218
|
-
requiresAuth
|
|
3219
|
-
});
|
|
3220
|
-
}
|
|
3221
|
-
}
|
|
3222
|
-
return scenarios;
|
|
3223
|
-
}
|
|
3224
|
-
function importFromOpenAPI(filePathOrUrl, projectId) {
|
|
3225
|
-
const inputs = parseOpenAPISpec(filePathOrUrl);
|
|
3226
|
-
const scenarios = inputs.map((input) => createScenario({ ...input, projectId }));
|
|
3227
|
-
return { imported: scenarios.length, scenarios };
|
|
3228
|
-
}
|
|
3229
|
-
var init_openapi_import = __esm(() => {
|
|
3230
|
-
init_scenarios();
|
|
3231
|
-
});
|
|
3232
|
-
|
|
3233
|
-
// src/lib/recorder.ts
|
|
3234
|
-
var exports_recorder = {};
|
|
3235
|
-
__export(exports_recorder, {
|
|
3236
|
-
recordSession: () => recordSession,
|
|
3237
|
-
recordAndSave: () => recordAndSave,
|
|
3238
|
-
actionsToScenarioInput: () => actionsToScenarioInput
|
|
3239
|
-
});
|
|
3240
|
-
import { chromium as chromium3 } from "playwright";
|
|
3241
|
-
async function recordSession(url, options) {
|
|
3242
|
-
const browser = await chromium3.launch({ headless: false });
|
|
3243
|
-
const context = await browser.newContext({ viewport: { width: 1280, height: 720 } });
|
|
3244
|
-
const page = await context.newPage();
|
|
3245
|
-
const actions = [];
|
|
3246
|
-
const startTime = Date.now();
|
|
3247
|
-
const timeout = options?.timeout ?? 300000;
|
|
3248
|
-
page.on("framenavigated", (frame) => {
|
|
3249
|
-
if (frame === page.mainFrame()) {
|
|
3250
|
-
actions.push({ type: "navigate", url: frame.url(), timestamp: Date.now() - startTime });
|
|
3251
|
-
}
|
|
3252
|
-
});
|
|
3253
|
-
await page.addInitScript(() => {
|
|
3254
|
-
document.addEventListener("click", (e) => {
|
|
3255
|
-
const target = e.target;
|
|
3256
|
-
const selector = buildSelector(target);
|
|
3257
|
-
window.postMessage({ __testers_action: "click", selector }, "*");
|
|
3258
|
-
}, true);
|
|
3259
|
-
document.addEventListener("input", (e) => {
|
|
3260
|
-
const target = e.target;
|
|
3261
|
-
const selector = buildSelector(target);
|
|
3262
|
-
window.postMessage({ __testers_action: "fill", selector, value: target.value }, "*");
|
|
3263
|
-
}, true);
|
|
3264
|
-
document.addEventListener("change", (e) => {
|
|
3265
|
-
const target = e.target;
|
|
3266
|
-
if (target.tagName === "SELECT") {
|
|
3267
|
-
const selector = buildSelector(target);
|
|
3268
|
-
window.postMessage({ __testers_action: "select", selector, value: target.value }, "*");
|
|
3269
|
-
}
|
|
3270
|
-
}, true);
|
|
3271
|
-
document.addEventListener("keydown", (e) => {
|
|
3272
|
-
if (["Enter", "Tab", "Escape"].includes(e.key)) {
|
|
3273
|
-
window.postMessage({ __testers_action: "press", key: e.key }, "*");
|
|
3274
|
-
}
|
|
3275
|
-
}, true);
|
|
3276
|
-
function buildSelector(el) {
|
|
3277
|
-
if (el.id)
|
|
3278
|
-
return `#${el.id}`;
|
|
3279
|
-
if (el.getAttribute("data-testid"))
|
|
3280
|
-
return `[data-testid="${el.getAttribute("data-testid")}"]`;
|
|
3281
|
-
if (el.getAttribute("name"))
|
|
3282
|
-
return `${el.tagName.toLowerCase()}[name="${el.getAttribute("name")}"]`;
|
|
3283
|
-
if (el.getAttribute("aria-label"))
|
|
3284
|
-
return `[aria-label="${el.getAttribute("aria-label")}"]`;
|
|
3285
|
-
if (el.className && typeof el.className === "string") {
|
|
3286
|
-
const classes = el.className.trim().split(/\s+/).slice(0, 2).join(".");
|
|
3287
|
-
if (classes)
|
|
3288
|
-
return `${el.tagName.toLowerCase()}.${classes}`;
|
|
3289
|
-
}
|
|
3290
|
-
const text = el.textContent?.trim().slice(0, 30);
|
|
3291
|
-
if (text)
|
|
3292
|
-
return `text="${text}"`;
|
|
3293
|
-
return el.tagName.toLowerCase();
|
|
3294
|
-
}
|
|
3295
|
-
});
|
|
3296
|
-
const pollInterval = setInterval(async () => {
|
|
3297
|
-
try {
|
|
3298
|
-
const newActions = await page.evaluate(() => {
|
|
3299
|
-
const collected = window.__testers_collected ?? [];
|
|
3300
|
-
window.__testers_collected = [];
|
|
3301
|
-
return collected;
|
|
3302
|
-
});
|
|
3303
|
-
for (const a of newActions) {
|
|
3304
|
-
actions.push({
|
|
3305
|
-
type: a["type"],
|
|
3306
|
-
selector: a["selector"],
|
|
3307
|
-
value: a["value"],
|
|
3308
|
-
key: a["key"],
|
|
3309
|
-
timestamp: Date.now() - startTime
|
|
3310
|
-
});
|
|
3311
|
-
}
|
|
3312
|
-
} catch {}
|
|
3313
|
-
}, 500);
|
|
3314
|
-
await page.exposeFunction("__testersRecord", (action) => {
|
|
3315
|
-
actions.push({ ...action, timestamp: Date.now() - startTime });
|
|
3316
|
-
});
|
|
3317
|
-
await page.addInitScript(() => {
|
|
3318
|
-
window.addEventListener("message", (e) => {
|
|
3319
|
-
if (e.data?.__testers_action) {
|
|
3320
|
-
const { __testers_action, ...rest } = e.data;
|
|
3321
|
-
window.__testersRecord({ type: __testers_action, ...rest });
|
|
3322
|
-
}
|
|
3323
|
-
});
|
|
3324
|
-
});
|
|
3325
|
-
await page.goto(url);
|
|
3326
|
-
actions.push({ type: "navigate", url, timestamp: 0 });
|
|
3327
|
-
console.log(`
|
|
3328
|
-
Recording started. Interact with the browser.`);
|
|
3329
|
-
console.log(` Close the browser window or wait ${timeout / 1000}s to stop.
|
|
3330
|
-
`);
|
|
3331
|
-
await Promise.race([
|
|
3332
|
-
page.waitForEvent("close").catch(() => {}),
|
|
3333
|
-
context.waitForEvent("close").catch(() => {}),
|
|
3334
|
-
new Promise((resolve) => setTimeout(resolve, timeout))
|
|
3335
|
-
]);
|
|
3336
|
-
clearInterval(pollInterval);
|
|
3337
|
-
try {
|
|
3338
|
-
await browser.close();
|
|
3339
|
-
} catch {}
|
|
3340
|
-
return {
|
|
3341
|
-
actions,
|
|
3342
|
-
url,
|
|
3343
|
-
duration: Date.now() - startTime
|
|
3344
|
-
};
|
|
3345
|
-
}
|
|
3346
|
-
function actionsToScenarioInput(recording, name, projectId) {
|
|
3347
|
-
const steps = [];
|
|
3348
|
-
const seenFills = new Map;
|
|
3349
|
-
for (const action of recording.actions) {
|
|
3350
|
-
switch (action.type) {
|
|
3351
|
-
case "navigate":
|
|
3352
|
-
if (action.url)
|
|
3353
|
-
steps.push(`Navigate to ${action.url}`);
|
|
3354
|
-
break;
|
|
3355
|
-
case "click":
|
|
3356
|
-
if (action.selector)
|
|
3357
|
-
steps.push(`Click ${action.selector}`);
|
|
3358
|
-
break;
|
|
3359
|
-
case "fill":
|
|
3360
|
-
if (action.selector && action.value) {
|
|
3361
|
-
seenFills.set(action.selector, action.value);
|
|
3362
|
-
}
|
|
3363
|
-
break;
|
|
3364
|
-
case "select":
|
|
3365
|
-
if (action.selector && action.value)
|
|
3366
|
-
steps.push(`Select "${action.value}" in ${action.selector}`);
|
|
3367
|
-
break;
|
|
3368
|
-
case "press":
|
|
3369
|
-
if (action.key)
|
|
3370
|
-
steps.push(`Press ${action.key}`);
|
|
3371
|
-
break;
|
|
3372
|
-
}
|
|
3373
|
-
}
|
|
3374
|
-
for (const [selector, value] of seenFills) {
|
|
3375
|
-
steps.push(`Fill ${selector} with "${value}"`);
|
|
3376
|
-
}
|
|
3377
|
-
return {
|
|
3378
|
-
name,
|
|
3379
|
-
description: `Recorded session on ${recording.url} (${(recording.duration / 1000).toFixed(0)}s, ${recording.actions.length} actions)`,
|
|
3380
|
-
steps,
|
|
3381
|
-
tags: ["recorded"],
|
|
3382
|
-
projectId
|
|
3383
|
-
};
|
|
3384
|
-
}
|
|
3385
|
-
async function recordAndSave(url, name, projectId) {
|
|
3386
|
-
const recording = await recordSession(url);
|
|
3387
|
-
const input = actionsToScenarioInput(recording, name, projectId);
|
|
3388
|
-
const scenario = createScenario(input);
|
|
3389
|
-
return { recording, scenario };
|
|
3390
|
-
}
|
|
3391
|
-
var init_recorder = __esm(() => {
|
|
3392
|
-
init_scenarios();
|
|
3393
|
-
});
|
|
3394
|
-
|
|
3395
|
-
// node_modules/commander/esm.mjs
|
|
3396
|
-
var import__ = __toESM(require_commander(), 1);
|
|
3397
|
-
var {
|
|
3398
|
-
program,
|
|
3399
|
-
createCommand,
|
|
3400
|
-
createArgument,
|
|
3401
|
-
createOption,
|
|
3402
|
-
CommanderError,
|
|
3403
|
-
InvalidArgumentError,
|
|
3404
|
-
InvalidOptionArgumentError,
|
|
3405
|
-
Command,
|
|
3406
|
-
Argument,
|
|
3407
|
-
Option,
|
|
3408
|
-
Help
|
|
3409
|
-
} = import__.default;
|
|
3410
|
-
|
|
3411
|
-
// src/cli/index.tsx
|
|
3412
|
-
import chalk5 from "chalk";
|
|
3413
|
-
// package.json
|
|
3414
|
-
var package_default = {
|
|
3415
|
-
name: "@hasna/testers",
|
|
3416
|
-
version: "0.0.12",
|
|
3417
|
-
description: "AI-powered QA testing CLI \u2014 spawns cheap AI agents to test web apps with headless browsers",
|
|
3418
|
-
type: "module",
|
|
3419
|
-
main: "dist/index.js",
|
|
3420
|
-
types: "dist/index.d.ts",
|
|
3421
|
-
bin: {
|
|
3422
|
-
testers: "dist/cli/index.js",
|
|
3423
|
-
"testers-mcp": "dist/mcp/index.js",
|
|
3424
|
-
"testers-serve": "dist/server/index.js"
|
|
3425
|
-
},
|
|
3426
|
-
exports: {
|
|
3427
|
-
".": {
|
|
3428
|
-
types: "./dist/index.d.ts",
|
|
3429
|
-
import: "./dist/index.js"
|
|
3430
|
-
}
|
|
3431
|
-
},
|
|
3432
|
-
files: [
|
|
3433
|
-
"dist/",
|
|
3434
|
-
"dashboard/dist/",
|
|
3435
|
-
"LICENSE",
|
|
3436
|
-
"README.md"
|
|
3437
|
-
],
|
|
3438
|
-
scripts: {
|
|
3439
|
-
build: "bun run build:dashboard && bun run build:cli && bun run build:mcp && bun run build:server && bun run build:lib && bun run build:types",
|
|
3440
|
-
"build:cli": "bun build src/cli/index.tsx --outdir dist/cli --target bun --external ink --external react --external chalk --external @modelcontextprotocol/sdk --external @anthropic-ai/sdk --external playwright",
|
|
3441
|
-
"build:mcp": "bun build src/mcp/index.ts --outdir dist/mcp --target bun --external @modelcontextprotocol/sdk --external @anthropic-ai/sdk --external playwright",
|
|
3442
|
-
"build:server": "bun build src/server/index.ts --outdir dist/server --target bun --external @anthropic-ai/sdk --external playwright",
|
|
3443
|
-
"build:lib": "bun build src/index.ts --outdir dist --target bun --external playwright --external @anthropic-ai/sdk --external @modelcontextprotocol/sdk",
|
|
3444
|
-
"build:types": "NODE_OPTIONS='--max-old-space-size=8192' tsc --emitDeclarationOnly --outDir dist --skipLibCheck",
|
|
3445
|
-
"build:dashboard": "cd dashboard && bun run build",
|
|
3446
|
-
typecheck: "tsc --noEmit",
|
|
3447
|
-
test: "bun test",
|
|
3448
|
-
"dev:cli": "bun run src/cli/index.tsx",
|
|
3449
|
-
"dev:mcp": "bun run src/mcp/index.ts",
|
|
3450
|
-
"dev:serve": "bun run src/server/index.ts",
|
|
3451
|
-
prepublishOnly: "bun run build"
|
|
3452
|
-
},
|
|
3453
|
-
dependencies: {
|
|
3454
|
-
"@anthropic-ai/sdk": "^0.52.0",
|
|
3455
|
-
"@modelcontextprotocol/sdk": "^1.12.1",
|
|
3456
|
-
chalk: "^5.4.1",
|
|
3457
|
-
commander: "^13.1.0",
|
|
3458
|
-
ink: "^5.2.0",
|
|
3459
|
-
playwright: "^1.50.0",
|
|
3460
|
-
react: "^18.3.1",
|
|
3461
|
-
zod: "^3.24.2"
|
|
3462
|
-
},
|
|
3463
|
-
devDependencies: {
|
|
3464
|
-
"@types/bun": "latest",
|
|
3465
|
-
"@types/react": "^18.3.18",
|
|
3466
|
-
typescript: "^5.7.3"
|
|
3467
|
-
},
|
|
3468
|
-
engines: {
|
|
3469
|
-
bun: ">=1.0.0"
|
|
3470
|
-
},
|
|
3471
|
-
publishConfig: {
|
|
3472
|
-
access: "public",
|
|
3473
|
-
registry: "https://registry.npmjs.org/"
|
|
3474
|
-
},
|
|
3475
|
-
repository: {
|
|
3476
|
-
type: "git",
|
|
3477
|
-
url: "https://github.com/hasna/open-testers.git"
|
|
3478
|
-
},
|
|
3479
|
-
license: "MIT",
|
|
3480
|
-
keywords: [
|
|
3481
|
-
"testing",
|
|
3482
|
-
"qa",
|
|
3483
|
-
"ai",
|
|
3484
|
-
"playwright",
|
|
3485
|
-
"browser",
|
|
3486
|
-
"screenshot",
|
|
3487
|
-
"automation",
|
|
3488
|
-
"cli",
|
|
3489
|
-
"mcp"
|
|
3490
|
-
]
|
|
3491
|
-
};
|
|
3492
|
-
|
|
3493
|
-
// src/cli/index.tsx
|
|
3494
|
-
init_scenarios();
|
|
3495
|
-
init_runs();
|
|
3496
|
-
import { render, Box, Text, useInput, useApp } from "ink";
|
|
3497
|
-
import React, { useState } from "react";
|
|
3498
|
-
import { readFileSync as readFileSync6, readdirSync, writeFileSync as writeFileSync3 } from "fs";
|
|
3499
|
-
import { createInterface } from "readline";
|
|
3500
|
-
import { join as join6, resolve } from "path";
|
|
3501
|
-
|
|
3502
|
-
// src/db/results.ts
|
|
3503
|
-
init_types();
|
|
3504
|
-
init_database();
|
|
3505
|
-
function createResult(input) {
|
|
3506
|
-
const db2 = getDatabase();
|
|
3507
|
-
const id = uuid();
|
|
3508
|
-
const timestamp = now();
|
|
3509
|
-
db2.query(`
|
|
3510
|
-
INSERT INTO results (id, run_id, scenario_id, status, reasoning, error, steps_completed, steps_total, duration_ms, model, tokens_used, cost_cents, metadata, created_at)
|
|
3511
|
-
VALUES (?, ?, ?, 'skipped', NULL, NULL, 0, ?, 0, ?, 0, 0, '{}', ?)
|
|
3512
|
-
`).run(id, input.runId, input.scenarioId, input.stepsTotal, input.model, timestamp);
|
|
3513
|
-
return getResult(id);
|
|
3514
|
-
}
|
|
3515
|
-
function getResult(id) {
|
|
3516
|
-
const db2 = getDatabase();
|
|
3517
|
-
let row = db2.query("SELECT * FROM results WHERE id = ?").get(id);
|
|
3518
|
-
if (row)
|
|
3519
|
-
return resultFromRow(row);
|
|
3520
|
-
const fullId = resolvePartialId("results", id);
|
|
3521
|
-
if (fullId) {
|
|
3522
|
-
row = db2.query("SELECT * FROM results WHERE id = ?").get(fullId);
|
|
3523
|
-
if (row)
|
|
3524
|
-
return resultFromRow(row);
|
|
3525
|
-
}
|
|
3526
|
-
return null;
|
|
3527
|
-
}
|
|
3528
|
-
function listResults(runId) {
|
|
3529
|
-
const db2 = getDatabase();
|
|
3530
|
-
const rows = db2.query("SELECT * FROM results WHERE run_id = ? ORDER BY created_at ASC").all(runId);
|
|
3531
|
-
return rows.map(resultFromRow);
|
|
3532
|
-
}
|
|
3533
|
-
function updateResult(id, updates) {
|
|
3534
|
-
const db2 = getDatabase();
|
|
3535
|
-
const existing = getResult(id);
|
|
3536
|
-
if (!existing) {
|
|
3537
|
-
throw new Error(`Result not found: ${id}`);
|
|
3538
|
-
}
|
|
3539
|
-
const sets = [];
|
|
3540
|
-
const params = [];
|
|
3541
|
-
if (updates.status !== undefined) {
|
|
3542
|
-
sets.push("status = ?");
|
|
3543
|
-
params.push(updates.status);
|
|
3544
|
-
}
|
|
3545
|
-
if (updates.reasoning !== undefined) {
|
|
3546
|
-
sets.push("reasoning = ?");
|
|
3547
|
-
params.push(updates.reasoning);
|
|
3548
|
-
}
|
|
3549
|
-
if (updates.error !== undefined) {
|
|
3550
|
-
sets.push("error = ?");
|
|
3551
|
-
params.push(updates.error);
|
|
3552
|
-
}
|
|
3553
|
-
if (updates.stepsCompleted !== undefined) {
|
|
3554
|
-
sets.push("steps_completed = ?");
|
|
3555
|
-
params.push(updates.stepsCompleted);
|
|
3556
|
-
}
|
|
3557
|
-
if (updates.durationMs !== undefined) {
|
|
3558
|
-
sets.push("duration_ms = ?");
|
|
3559
|
-
params.push(updates.durationMs);
|
|
3560
|
-
}
|
|
3561
|
-
if (updates.tokensUsed !== undefined) {
|
|
3562
|
-
sets.push("tokens_used = ?");
|
|
3563
|
-
params.push(updates.tokensUsed);
|
|
3564
|
-
}
|
|
3565
|
-
if (updates.costCents !== undefined) {
|
|
3566
|
-
sets.push("cost_cents = ?");
|
|
3567
|
-
params.push(updates.costCents);
|
|
3568
|
-
}
|
|
3569
|
-
if (sets.length === 0) {
|
|
3570
|
-
return existing;
|
|
3571
|
-
}
|
|
3572
|
-
params.push(existing.id);
|
|
3573
|
-
db2.query(`UPDATE results SET ${sets.join(", ")} WHERE id = ?`).run(...params);
|
|
3574
|
-
return getResult(existing.id);
|
|
3575
|
-
}
|
|
3576
|
-
function getResultsByRun(runId) {
|
|
3577
|
-
return listResults(runId);
|
|
3578
|
-
}
|
|
3579
|
-
|
|
3580
|
-
// src/db/screenshots.ts
|
|
3581
|
-
init_types();
|
|
3582
|
-
init_database();
|
|
3583
|
-
function createScreenshot(input) {
|
|
3584
|
-
const db2 = getDatabase();
|
|
3585
|
-
const id = uuid();
|
|
3586
|
-
const timestamp = now();
|
|
3587
|
-
db2.query(`
|
|
3588
|
-
INSERT INTO screenshots (id, result_id, step_number, action, file_path, width, height, timestamp, description, page_url, thumbnail_path)
|
|
3589
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
3590
|
-
`).run(id, input.resultId, input.stepNumber, input.action, input.filePath, input.width, input.height, timestamp, input.description ?? null, input.pageUrl ?? null, input.thumbnailPath ?? null);
|
|
3591
|
-
return getScreenshot(id);
|
|
3592
|
-
}
|
|
3593
|
-
function getScreenshot(id) {
|
|
3594
|
-
const db2 = getDatabase();
|
|
3595
|
-
const row = db2.query("SELECT * FROM screenshots WHERE id = ?").get(id);
|
|
3596
|
-
return row ? screenshotFromRow(row) : null;
|
|
3597
|
-
}
|
|
3598
|
-
function listScreenshots(resultId) {
|
|
3599
|
-
const db2 = getDatabase();
|
|
3600
|
-
const rows = db2.query("SELECT * FROM screenshots WHERE result_id = ? ORDER BY step_number ASC").all(resultId);
|
|
3601
|
-
return rows.map(screenshotFromRow);
|
|
3602
|
-
}
|
|
3603
|
-
|
|
3604
|
-
// src/lib/runner.ts
|
|
3605
|
-
init_runs();
|
|
3606
|
-
init_scenarios();
|
|
3607
|
-
|
|
3608
|
-
// src/lib/browser.ts
|
|
3609
|
-
init_types();
|
|
3610
|
-
import { chromium as chromium2 } from "playwright";
|
|
3611
|
-
import { execSync } from "child_process";
|
|
3612
|
-
var DEFAULT_VIEWPORT = { width: 1280, height: 720 };
|
|
3613
|
-
async function launchBrowser(options) {
|
|
3614
|
-
const engine = options?.engine ?? process.env["TESTERS_BROWSER_ENGINE"] ?? "playwright";
|
|
3615
|
-
if (engine === "lightpanda") {
|
|
3616
|
-
const { launchLightpanda: launchLightpanda2, isLightpandaAvailable: isLightpandaAvailable2 } = await Promise.resolve().then(() => (init_browser_lightpanda(), exports_browser_lightpanda));
|
|
3617
|
-
if (!isLightpandaAvailable2()) {
|
|
3618
|
-
throw new BrowserError("Lightpanda not installed. Run: testers install-browser --engine lightpanda");
|
|
3619
|
-
}
|
|
3620
|
-
return launchLightpanda2({ viewport: options?.viewport });
|
|
3621
|
-
}
|
|
3622
|
-
const headless = options?.headless ?? true;
|
|
3623
|
-
const viewport = options?.viewport ?? DEFAULT_VIEWPORT;
|
|
3624
|
-
try {
|
|
3625
|
-
const browser = await chromium2.launch({
|
|
3626
|
-
headless,
|
|
3627
|
-
args: [
|
|
3628
|
-
`--window-size=${viewport.width},${viewport.height}`
|
|
3629
|
-
]
|
|
3630
|
-
});
|
|
3631
|
-
return browser;
|
|
3632
|
-
} catch (error) {
|
|
3633
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
3634
|
-
throw new BrowserError(`Failed to launch browser: ${message}`);
|
|
3635
|
-
}
|
|
3636
|
-
}
|
|
3637
|
-
async function getPage(browser, options) {
|
|
3638
|
-
const engine = options?.engine ?? "playwright";
|
|
3639
|
-
if (engine === "lightpanda") {
|
|
3640
|
-
const { getLightpandaPage: getLightpandaPage2 } = await Promise.resolve().then(() => (init_browser_lightpanda(), exports_browser_lightpanda));
|
|
3641
|
-
return getLightpandaPage2(browser, options);
|
|
3642
|
-
}
|
|
3643
|
-
const viewport = options?.viewport ?? DEFAULT_VIEWPORT;
|
|
3644
|
-
try {
|
|
3645
|
-
const context = await browser.newContext({
|
|
3646
|
-
viewport,
|
|
3647
|
-
userAgent: options?.userAgent,
|
|
3648
|
-
locale: options?.locale
|
|
3649
|
-
});
|
|
3650
|
-
const page = await context.newPage();
|
|
3651
|
-
return page;
|
|
3652
|
-
} catch (error) {
|
|
3653
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
3654
|
-
throw new BrowserError(`Failed to create page: ${message}`);
|
|
3655
|
-
}
|
|
3656
|
-
}
|
|
3657
|
-
async function closeBrowser(browser, engine) {
|
|
3658
|
-
if (engine === "lightpanda") {
|
|
3659
|
-
const { closeLightpanda: closeLightpanda2 } = await Promise.resolve().then(() => (init_browser_lightpanda(), exports_browser_lightpanda));
|
|
3660
|
-
return closeLightpanda2(browser);
|
|
3661
|
-
}
|
|
3662
|
-
try {
|
|
3663
|
-
await browser.close();
|
|
3664
|
-
} catch (error) {
|
|
3665
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
3666
|
-
throw new BrowserError(`Failed to close browser: ${message}`);
|
|
3667
|
-
}
|
|
3668
|
-
}
|
|
3669
|
-
async function installBrowser(engine) {
|
|
3670
|
-
if (engine === "lightpanda") {
|
|
3671
|
-
const { installLightpanda: installLightpanda2 } = await Promise.resolve().then(() => (init_browser_lightpanda(), exports_browser_lightpanda));
|
|
3672
|
-
return installLightpanda2();
|
|
3196
|
+
}
|
|
3197
|
+
async function installBrowser(engine) {
|
|
3198
|
+
if (engine === "lightpanda") {
|
|
3199
|
+
const { installLightpanda: installLightpanda2 } = await Promise.resolve().then(() => (init_browser_lightpanda(), exports_browser_lightpanda));
|
|
3200
|
+
return installLightpanda2();
|
|
3673
3201
|
}
|
|
3674
3202
|
try {
|
|
3675
3203
|
execSync("bunx playwright install chromium", {
|
|
@@ -3680,6 +3208,11 @@ async function installBrowser(engine) {
|
|
|
3680
3208
|
throw new BrowserError(`Failed to install browser: ${message}`);
|
|
3681
3209
|
}
|
|
3682
3210
|
}
|
|
3211
|
+
var DEFAULT_VIEWPORT;
|
|
3212
|
+
var init_browser = __esm(() => {
|
|
3213
|
+
init_types();
|
|
3214
|
+
DEFAULT_VIEWPORT = { width: 1280, height: 720 };
|
|
3215
|
+
});
|
|
3683
3216
|
|
|
3684
3217
|
// src/lib/screenshotter.ts
|
|
3685
3218
|
import { mkdirSync as mkdirSync2, existsSync as existsSync2, writeFileSync } from "fs";
|
|
@@ -3736,7 +3269,6 @@ async function generateThumbnail(page, screenshotDir, filename) {
|
|
|
3736
3269
|
return null;
|
|
3737
3270
|
}
|
|
3738
3271
|
}
|
|
3739
|
-
var DEFAULT_BASE_DIR = join2(homedir2(), ".testers", "screenshots");
|
|
3740
3272
|
|
|
3741
3273
|
class Screenshotter {
|
|
3742
3274
|
baseDir;
|
|
@@ -3857,320 +3389,19 @@ class Screenshotter {
|
|
|
3857
3389
|
};
|
|
3858
3390
|
}
|
|
3859
3391
|
}
|
|
3392
|
+
var DEFAULT_BASE_DIR;
|
|
3393
|
+
var init_screenshotter = __esm(() => {
|
|
3394
|
+
DEFAULT_BASE_DIR = join2(homedir2(), ".testers", "screenshots");
|
|
3395
|
+
});
|
|
3860
3396
|
|
|
3861
|
-
// src/lib/ai-client.ts
|
|
3862
|
-
|
|
3863
|
-
|
|
3864
|
-
|
|
3865
|
-
|
|
3866
|
-
return MODEL_MAP[nameOrPreset];
|
|
3867
|
-
}
|
|
3868
|
-
return nameOrPreset;
|
|
3869
|
-
}
|
|
3870
|
-
var BROWSER_TOOLS = [
|
|
3871
|
-
{
|
|
3872
|
-
name: "navigate",
|
|
3873
|
-
description: "Navigate the browser to a specific URL.",
|
|
3874
|
-
input_schema: {
|
|
3875
|
-
type: "object",
|
|
3876
|
-
properties: {
|
|
3877
|
-
url: { type: "string", description: "The URL to navigate to." }
|
|
3878
|
-
},
|
|
3879
|
-
required: ["url"]
|
|
3880
|
-
}
|
|
3881
|
-
},
|
|
3882
|
-
{
|
|
3883
|
-
name: "click",
|
|
3884
|
-
description: "Click on an element matching the given CSS selector.",
|
|
3885
|
-
input_schema: {
|
|
3886
|
-
type: "object",
|
|
3887
|
-
properties: {
|
|
3888
|
-
selector: {
|
|
3889
|
-
type: "string",
|
|
3890
|
-
description: "CSS selector of the element to click."
|
|
3891
|
-
}
|
|
3892
|
-
},
|
|
3893
|
-
required: ["selector"]
|
|
3894
|
-
}
|
|
3895
|
-
},
|
|
3896
|
-
{
|
|
3897
|
-
name: "fill",
|
|
3898
|
-
description: "Fill an input field with the given value.",
|
|
3899
|
-
input_schema: {
|
|
3900
|
-
type: "object",
|
|
3901
|
-
properties: {
|
|
3902
|
-
selector: {
|
|
3903
|
-
type: "string",
|
|
3904
|
-
description: "CSS selector of the input field."
|
|
3905
|
-
},
|
|
3906
|
-
value: {
|
|
3907
|
-
type: "string",
|
|
3908
|
-
description: "The value to fill into the input."
|
|
3909
|
-
}
|
|
3910
|
-
},
|
|
3911
|
-
required: ["selector", "value"]
|
|
3912
|
-
}
|
|
3913
|
-
},
|
|
3914
|
-
{
|
|
3915
|
-
name: "select_option",
|
|
3916
|
-
description: "Select an option from a dropdown/select element.",
|
|
3917
|
-
input_schema: {
|
|
3918
|
-
type: "object",
|
|
3919
|
-
properties: {
|
|
3920
|
-
selector: {
|
|
3921
|
-
type: "string",
|
|
3922
|
-
description: "CSS selector of the select element."
|
|
3923
|
-
},
|
|
3924
|
-
value: {
|
|
3925
|
-
type: "string",
|
|
3926
|
-
description: "The value of the option to select."
|
|
3927
|
-
}
|
|
3928
|
-
},
|
|
3929
|
-
required: ["selector", "value"]
|
|
3930
|
-
}
|
|
3931
|
-
},
|
|
3932
|
-
{
|
|
3933
|
-
name: "screenshot",
|
|
3934
|
-
description: "Take a screenshot of the current page state.",
|
|
3935
|
-
input_schema: {
|
|
3936
|
-
type: "object",
|
|
3937
|
-
properties: {},
|
|
3938
|
-
required: []
|
|
3939
|
-
}
|
|
3940
|
-
},
|
|
3941
|
-
{
|
|
3942
|
-
name: "get_text",
|
|
3943
|
-
description: "Get the text content of an element matching the selector.",
|
|
3944
|
-
input_schema: {
|
|
3945
|
-
type: "object",
|
|
3946
|
-
properties: {
|
|
3947
|
-
selector: {
|
|
3948
|
-
type: "string",
|
|
3949
|
-
description: "CSS selector of the element."
|
|
3950
|
-
}
|
|
3951
|
-
},
|
|
3952
|
-
required: ["selector"]
|
|
3953
|
-
}
|
|
3954
|
-
},
|
|
3955
|
-
{
|
|
3956
|
-
name: "get_url",
|
|
3957
|
-
description: "Get the current page URL.",
|
|
3958
|
-
input_schema: {
|
|
3959
|
-
type: "object",
|
|
3960
|
-
properties: {},
|
|
3961
|
-
required: []
|
|
3962
|
-
}
|
|
3963
|
-
},
|
|
3964
|
-
{
|
|
3965
|
-
name: "wait_for",
|
|
3966
|
-
description: "Wait for an element matching the selector to appear on the page.",
|
|
3967
|
-
input_schema: {
|
|
3968
|
-
type: "object",
|
|
3969
|
-
properties: {
|
|
3970
|
-
selector: {
|
|
3971
|
-
type: "string",
|
|
3972
|
-
description: "CSS selector to wait for."
|
|
3973
|
-
},
|
|
3974
|
-
timeout: {
|
|
3975
|
-
type: "number",
|
|
3976
|
-
description: "Maximum time to wait in milliseconds (default: 10000)."
|
|
3977
|
-
}
|
|
3978
|
-
},
|
|
3979
|
-
required: ["selector"]
|
|
3980
|
-
}
|
|
3981
|
-
},
|
|
3982
|
-
{
|
|
3983
|
-
name: "go_back",
|
|
3984
|
-
description: "Navigate back to the previous page.",
|
|
3985
|
-
input_schema: {
|
|
3986
|
-
type: "object",
|
|
3987
|
-
properties: {},
|
|
3988
|
-
required: []
|
|
3989
|
-
}
|
|
3990
|
-
},
|
|
3991
|
-
{
|
|
3992
|
-
name: "press_key",
|
|
3993
|
-
description: "Press a keyboard key (e.g., Enter, Tab, Escape, ArrowDown).",
|
|
3994
|
-
input_schema: {
|
|
3995
|
-
type: "object",
|
|
3996
|
-
properties: {
|
|
3997
|
-
key: {
|
|
3998
|
-
type: "string",
|
|
3999
|
-
description: "The key to press (e.g., 'Enter', 'Tab', 'Escape')."
|
|
4000
|
-
}
|
|
4001
|
-
},
|
|
4002
|
-
required: ["key"]
|
|
4003
|
-
}
|
|
4004
|
-
},
|
|
4005
|
-
{
|
|
4006
|
-
name: "assert_visible",
|
|
4007
|
-
description: "Assert that an element matching the selector is visible on the page. Returns 'true' or 'false'.",
|
|
4008
|
-
input_schema: {
|
|
4009
|
-
type: "object",
|
|
4010
|
-
properties: {
|
|
4011
|
-
selector: {
|
|
4012
|
-
type: "string",
|
|
4013
|
-
description: "CSS selector of the element to check."
|
|
4014
|
-
}
|
|
4015
|
-
},
|
|
4016
|
-
required: ["selector"]
|
|
4017
|
-
}
|
|
4018
|
-
},
|
|
4019
|
-
{
|
|
4020
|
-
name: "assert_text",
|
|
4021
|
-
description: "Assert that the given text is visible somewhere on the page. Returns 'true' or 'false'.",
|
|
4022
|
-
input_schema: {
|
|
4023
|
-
type: "object",
|
|
4024
|
-
properties: {
|
|
4025
|
-
text: {
|
|
4026
|
-
type: "string",
|
|
4027
|
-
description: "The text to search for on the page."
|
|
4028
|
-
}
|
|
4029
|
-
},
|
|
4030
|
-
required: ["text"]
|
|
4031
|
-
}
|
|
4032
|
-
},
|
|
4033
|
-
{
|
|
4034
|
-
name: "scroll",
|
|
4035
|
-
description: "Scroll the page up or down by a given amount of pixels.",
|
|
4036
|
-
input_schema: {
|
|
4037
|
-
type: "object",
|
|
4038
|
-
properties: {
|
|
4039
|
-
direction: {
|
|
4040
|
-
type: "string",
|
|
4041
|
-
enum: ["up", "down"],
|
|
4042
|
-
description: "Direction to scroll."
|
|
4043
|
-
},
|
|
4044
|
-
amount: {
|
|
4045
|
-
type: "number",
|
|
4046
|
-
description: "Number of pixels to scroll (default: 500)."
|
|
4047
|
-
}
|
|
4048
|
-
},
|
|
4049
|
-
required: ["direction"]
|
|
4050
|
-
}
|
|
4051
|
-
},
|
|
4052
|
-
{
|
|
4053
|
-
name: "get_page_html",
|
|
4054
|
-
description: "Get simplified HTML of the page body content, truncated to 8000 characters.",
|
|
4055
|
-
input_schema: {
|
|
4056
|
-
type: "object",
|
|
4057
|
-
properties: {},
|
|
4058
|
-
required: []
|
|
4059
|
-
}
|
|
4060
|
-
},
|
|
4061
|
-
{
|
|
4062
|
-
name: "get_elements",
|
|
4063
|
-
description: "List elements matching a CSS selector with their text, tag name, and key attributes (max 20 results).",
|
|
4064
|
-
input_schema: {
|
|
4065
|
-
type: "object",
|
|
4066
|
-
properties: {
|
|
4067
|
-
selector: {
|
|
4068
|
-
type: "string",
|
|
4069
|
-
description: "CSS selector to match elements."
|
|
4070
|
-
}
|
|
4071
|
-
},
|
|
4072
|
-
required: ["selector"]
|
|
4073
|
-
}
|
|
4074
|
-
},
|
|
4075
|
-
{
|
|
4076
|
-
name: "wait_for_navigation",
|
|
4077
|
-
description: "Wait for page navigation/load to complete (network idle).",
|
|
4078
|
-
input_schema: {
|
|
4079
|
-
type: "object",
|
|
4080
|
-
properties: {
|
|
4081
|
-
timeout: {
|
|
4082
|
-
type: "number",
|
|
4083
|
-
description: "Maximum time to wait in milliseconds (default: 10000)."
|
|
4084
|
-
}
|
|
4085
|
-
},
|
|
4086
|
-
required: []
|
|
4087
|
-
}
|
|
4088
|
-
},
|
|
4089
|
-
{
|
|
4090
|
-
name: "get_page_title",
|
|
4091
|
-
description: "Get the document title of the current page.",
|
|
4092
|
-
input_schema: {
|
|
4093
|
-
type: "object",
|
|
4094
|
-
properties: {},
|
|
4095
|
-
required: []
|
|
4096
|
-
}
|
|
4097
|
-
},
|
|
4098
|
-
{
|
|
4099
|
-
name: "count_elements",
|
|
4100
|
-
description: "Count the number of elements matching a CSS selector.",
|
|
4101
|
-
input_schema: {
|
|
4102
|
-
type: "object",
|
|
4103
|
-
properties: {
|
|
4104
|
-
selector: {
|
|
4105
|
-
type: "string",
|
|
4106
|
-
description: "CSS selector to count matching elements."
|
|
4107
|
-
}
|
|
4108
|
-
},
|
|
4109
|
-
required: ["selector"]
|
|
4110
|
-
}
|
|
4111
|
-
},
|
|
4112
|
-
{
|
|
4113
|
-
name: "hover",
|
|
4114
|
-
description: "Hover over an element matching the given CSS selector.",
|
|
4115
|
-
input_schema: {
|
|
4116
|
-
type: "object",
|
|
4117
|
-
properties: {
|
|
4118
|
-
selector: {
|
|
4119
|
-
type: "string",
|
|
4120
|
-
description: "CSS selector of the element to hover over."
|
|
4121
|
-
}
|
|
4122
|
-
},
|
|
4123
|
-
required: ["selector"]
|
|
4124
|
-
}
|
|
4125
|
-
},
|
|
4126
|
-
{
|
|
4127
|
-
name: "check",
|
|
4128
|
-
description: "Check a checkbox matching the given CSS selector.",
|
|
4129
|
-
input_schema: {
|
|
4130
|
-
type: "object",
|
|
4131
|
-
properties: {
|
|
4132
|
-
selector: {
|
|
4133
|
-
type: "string",
|
|
4134
|
-
description: "CSS selector of the checkbox to check."
|
|
4135
|
-
}
|
|
4136
|
-
},
|
|
4137
|
-
required: ["selector"]
|
|
4138
|
-
}
|
|
4139
|
-
},
|
|
4140
|
-
{
|
|
4141
|
-
name: "uncheck",
|
|
4142
|
-
description: "Uncheck a checkbox matching the given CSS selector.",
|
|
4143
|
-
input_schema: {
|
|
4144
|
-
type: "object",
|
|
4145
|
-
properties: {
|
|
4146
|
-
selector: {
|
|
4147
|
-
type: "string",
|
|
4148
|
-
description: "CSS selector of the checkbox to uncheck."
|
|
4149
|
-
}
|
|
4150
|
-
},
|
|
4151
|
-
required: ["selector"]
|
|
4152
|
-
}
|
|
4153
|
-
},
|
|
4154
|
-
{
|
|
4155
|
-
name: "report_result",
|
|
4156
|
-
description: "Report the final test result. Call this when you have completed testing the scenario. This MUST be the last tool you call.",
|
|
4157
|
-
input_schema: {
|
|
4158
|
-
type: "object",
|
|
4159
|
-
properties: {
|
|
4160
|
-
status: {
|
|
4161
|
-
type: "string",
|
|
4162
|
-
enum: ["passed", "failed"],
|
|
4163
|
-
description: "Whether the test scenario passed or failed."
|
|
4164
|
-
},
|
|
4165
|
-
reasoning: {
|
|
4166
|
-
type: "string",
|
|
4167
|
-
description: "Detailed explanation of why the test passed or failed, including any issues found."
|
|
4168
|
-
}
|
|
4169
|
-
},
|
|
4170
|
-
required: ["status", "reasoning"]
|
|
4171
|
-
}
|
|
3397
|
+
// src/lib/ai-client.ts
|
|
3398
|
+
import Anthropic from "@anthropic-ai/sdk";
|
|
3399
|
+
function resolveModel(nameOrPreset) {
|
|
3400
|
+
if (nameOrPreset in MODEL_MAP) {
|
|
3401
|
+
return MODEL_MAP[nameOrPreset];
|
|
4172
3402
|
}
|
|
4173
|
-
|
|
3403
|
+
return nameOrPreset;
|
|
3404
|
+
}
|
|
4174
3405
|
async function executeTool(page, screenshotter, toolName, toolInput, context) {
|
|
4175
3406
|
try {
|
|
4176
3407
|
switch (toolName) {
|
|
@@ -4549,14 +3780,319 @@ function createClient(apiKey) {
|
|
|
4549
3780
|
}
|
|
4550
3781
|
return new Anthropic({ apiKey: key });
|
|
4551
3782
|
}
|
|
3783
|
+
var BROWSER_TOOLS;
|
|
3784
|
+
var init_ai_client = __esm(() => {
|
|
3785
|
+
init_types();
|
|
3786
|
+
BROWSER_TOOLS = [
|
|
3787
|
+
{
|
|
3788
|
+
name: "navigate",
|
|
3789
|
+
description: "Navigate the browser to a specific URL.",
|
|
3790
|
+
input_schema: {
|
|
3791
|
+
type: "object",
|
|
3792
|
+
properties: {
|
|
3793
|
+
url: { type: "string", description: "The URL to navigate to." }
|
|
3794
|
+
},
|
|
3795
|
+
required: ["url"]
|
|
3796
|
+
}
|
|
3797
|
+
},
|
|
3798
|
+
{
|
|
3799
|
+
name: "click",
|
|
3800
|
+
description: "Click on an element matching the given CSS selector.",
|
|
3801
|
+
input_schema: {
|
|
3802
|
+
type: "object",
|
|
3803
|
+
properties: {
|
|
3804
|
+
selector: {
|
|
3805
|
+
type: "string",
|
|
3806
|
+
description: "CSS selector of the element to click."
|
|
3807
|
+
}
|
|
3808
|
+
},
|
|
3809
|
+
required: ["selector"]
|
|
3810
|
+
}
|
|
3811
|
+
},
|
|
3812
|
+
{
|
|
3813
|
+
name: "fill",
|
|
3814
|
+
description: "Fill an input field with the given value.",
|
|
3815
|
+
input_schema: {
|
|
3816
|
+
type: "object",
|
|
3817
|
+
properties: {
|
|
3818
|
+
selector: {
|
|
3819
|
+
type: "string",
|
|
3820
|
+
description: "CSS selector of the input field."
|
|
3821
|
+
},
|
|
3822
|
+
value: {
|
|
3823
|
+
type: "string",
|
|
3824
|
+
description: "The value to fill into the input."
|
|
3825
|
+
}
|
|
3826
|
+
},
|
|
3827
|
+
required: ["selector", "value"]
|
|
3828
|
+
}
|
|
3829
|
+
},
|
|
3830
|
+
{
|
|
3831
|
+
name: "select_option",
|
|
3832
|
+
description: "Select an option from a dropdown/select element.",
|
|
3833
|
+
input_schema: {
|
|
3834
|
+
type: "object",
|
|
3835
|
+
properties: {
|
|
3836
|
+
selector: {
|
|
3837
|
+
type: "string",
|
|
3838
|
+
description: "CSS selector of the select element."
|
|
3839
|
+
},
|
|
3840
|
+
value: {
|
|
3841
|
+
type: "string",
|
|
3842
|
+
description: "The value of the option to select."
|
|
3843
|
+
}
|
|
3844
|
+
},
|
|
3845
|
+
required: ["selector", "value"]
|
|
3846
|
+
}
|
|
3847
|
+
},
|
|
3848
|
+
{
|
|
3849
|
+
name: "screenshot",
|
|
3850
|
+
description: "Take a screenshot of the current page state.",
|
|
3851
|
+
input_schema: {
|
|
3852
|
+
type: "object",
|
|
3853
|
+
properties: {},
|
|
3854
|
+
required: []
|
|
3855
|
+
}
|
|
3856
|
+
},
|
|
3857
|
+
{
|
|
3858
|
+
name: "get_text",
|
|
3859
|
+
description: "Get the text content of an element matching the selector.",
|
|
3860
|
+
input_schema: {
|
|
3861
|
+
type: "object",
|
|
3862
|
+
properties: {
|
|
3863
|
+
selector: {
|
|
3864
|
+
type: "string",
|
|
3865
|
+
description: "CSS selector of the element."
|
|
3866
|
+
}
|
|
3867
|
+
},
|
|
3868
|
+
required: ["selector"]
|
|
3869
|
+
}
|
|
3870
|
+
},
|
|
3871
|
+
{
|
|
3872
|
+
name: "get_url",
|
|
3873
|
+
description: "Get the current page URL.",
|
|
3874
|
+
input_schema: {
|
|
3875
|
+
type: "object",
|
|
3876
|
+
properties: {},
|
|
3877
|
+
required: []
|
|
3878
|
+
}
|
|
3879
|
+
},
|
|
3880
|
+
{
|
|
3881
|
+
name: "wait_for",
|
|
3882
|
+
description: "Wait for an element matching the selector to appear on the page.",
|
|
3883
|
+
input_schema: {
|
|
3884
|
+
type: "object",
|
|
3885
|
+
properties: {
|
|
3886
|
+
selector: {
|
|
3887
|
+
type: "string",
|
|
3888
|
+
description: "CSS selector to wait for."
|
|
3889
|
+
},
|
|
3890
|
+
timeout: {
|
|
3891
|
+
type: "number",
|
|
3892
|
+
description: "Maximum time to wait in milliseconds (default: 10000)."
|
|
3893
|
+
}
|
|
3894
|
+
},
|
|
3895
|
+
required: ["selector"]
|
|
3896
|
+
}
|
|
3897
|
+
},
|
|
3898
|
+
{
|
|
3899
|
+
name: "go_back",
|
|
3900
|
+
description: "Navigate back to the previous page.",
|
|
3901
|
+
input_schema: {
|
|
3902
|
+
type: "object",
|
|
3903
|
+
properties: {},
|
|
3904
|
+
required: []
|
|
3905
|
+
}
|
|
3906
|
+
},
|
|
3907
|
+
{
|
|
3908
|
+
name: "press_key",
|
|
3909
|
+
description: "Press a keyboard key (e.g., Enter, Tab, Escape, ArrowDown).",
|
|
3910
|
+
input_schema: {
|
|
3911
|
+
type: "object",
|
|
3912
|
+
properties: {
|
|
3913
|
+
key: {
|
|
3914
|
+
type: "string",
|
|
3915
|
+
description: "The key to press (e.g., 'Enter', 'Tab', 'Escape')."
|
|
3916
|
+
}
|
|
3917
|
+
},
|
|
3918
|
+
required: ["key"]
|
|
3919
|
+
}
|
|
3920
|
+
},
|
|
3921
|
+
{
|
|
3922
|
+
name: "assert_visible",
|
|
3923
|
+
description: "Assert that an element matching the selector is visible on the page. Returns 'true' or 'false'.",
|
|
3924
|
+
input_schema: {
|
|
3925
|
+
type: "object",
|
|
3926
|
+
properties: {
|
|
3927
|
+
selector: {
|
|
3928
|
+
type: "string",
|
|
3929
|
+
description: "CSS selector of the element to check."
|
|
3930
|
+
}
|
|
3931
|
+
},
|
|
3932
|
+
required: ["selector"]
|
|
3933
|
+
}
|
|
3934
|
+
},
|
|
3935
|
+
{
|
|
3936
|
+
name: "assert_text",
|
|
3937
|
+
description: "Assert that the given text is visible somewhere on the page. Returns 'true' or 'false'.",
|
|
3938
|
+
input_schema: {
|
|
3939
|
+
type: "object",
|
|
3940
|
+
properties: {
|
|
3941
|
+
text: {
|
|
3942
|
+
type: "string",
|
|
3943
|
+
description: "The text to search for on the page."
|
|
3944
|
+
}
|
|
3945
|
+
},
|
|
3946
|
+
required: ["text"]
|
|
3947
|
+
}
|
|
3948
|
+
},
|
|
3949
|
+
{
|
|
3950
|
+
name: "scroll",
|
|
3951
|
+
description: "Scroll the page up or down by a given amount of pixels.",
|
|
3952
|
+
input_schema: {
|
|
3953
|
+
type: "object",
|
|
3954
|
+
properties: {
|
|
3955
|
+
direction: {
|
|
3956
|
+
type: "string",
|
|
3957
|
+
enum: ["up", "down"],
|
|
3958
|
+
description: "Direction to scroll."
|
|
3959
|
+
},
|
|
3960
|
+
amount: {
|
|
3961
|
+
type: "number",
|
|
3962
|
+
description: "Number of pixels to scroll (default: 500)."
|
|
3963
|
+
}
|
|
3964
|
+
},
|
|
3965
|
+
required: ["direction"]
|
|
3966
|
+
}
|
|
3967
|
+
},
|
|
3968
|
+
{
|
|
3969
|
+
name: "get_page_html",
|
|
3970
|
+
description: "Get simplified HTML of the page body content, truncated to 8000 characters.",
|
|
3971
|
+
input_schema: {
|
|
3972
|
+
type: "object",
|
|
3973
|
+
properties: {},
|
|
3974
|
+
required: []
|
|
3975
|
+
}
|
|
3976
|
+
},
|
|
3977
|
+
{
|
|
3978
|
+
name: "get_elements",
|
|
3979
|
+
description: "List elements matching a CSS selector with their text, tag name, and key attributes (max 20 results).",
|
|
3980
|
+
input_schema: {
|
|
3981
|
+
type: "object",
|
|
3982
|
+
properties: {
|
|
3983
|
+
selector: {
|
|
3984
|
+
type: "string",
|
|
3985
|
+
description: "CSS selector to match elements."
|
|
3986
|
+
}
|
|
3987
|
+
},
|
|
3988
|
+
required: ["selector"]
|
|
3989
|
+
}
|
|
3990
|
+
},
|
|
3991
|
+
{
|
|
3992
|
+
name: "wait_for_navigation",
|
|
3993
|
+
description: "Wait for page navigation/load to complete (network idle).",
|
|
3994
|
+
input_schema: {
|
|
3995
|
+
type: "object",
|
|
3996
|
+
properties: {
|
|
3997
|
+
timeout: {
|
|
3998
|
+
type: "number",
|
|
3999
|
+
description: "Maximum time to wait in milliseconds (default: 10000)."
|
|
4000
|
+
}
|
|
4001
|
+
},
|
|
4002
|
+
required: []
|
|
4003
|
+
}
|
|
4004
|
+
},
|
|
4005
|
+
{
|
|
4006
|
+
name: "get_page_title",
|
|
4007
|
+
description: "Get the document title of the current page.",
|
|
4008
|
+
input_schema: {
|
|
4009
|
+
type: "object",
|
|
4010
|
+
properties: {},
|
|
4011
|
+
required: []
|
|
4012
|
+
}
|
|
4013
|
+
},
|
|
4014
|
+
{
|
|
4015
|
+
name: "count_elements",
|
|
4016
|
+
description: "Count the number of elements matching a CSS selector.",
|
|
4017
|
+
input_schema: {
|
|
4018
|
+
type: "object",
|
|
4019
|
+
properties: {
|
|
4020
|
+
selector: {
|
|
4021
|
+
type: "string",
|
|
4022
|
+
description: "CSS selector to count matching elements."
|
|
4023
|
+
}
|
|
4024
|
+
},
|
|
4025
|
+
required: ["selector"]
|
|
4026
|
+
}
|
|
4027
|
+
},
|
|
4028
|
+
{
|
|
4029
|
+
name: "hover",
|
|
4030
|
+
description: "Hover over an element matching the given CSS selector.",
|
|
4031
|
+
input_schema: {
|
|
4032
|
+
type: "object",
|
|
4033
|
+
properties: {
|
|
4034
|
+
selector: {
|
|
4035
|
+
type: "string",
|
|
4036
|
+
description: "CSS selector of the element to hover over."
|
|
4037
|
+
}
|
|
4038
|
+
},
|
|
4039
|
+
required: ["selector"]
|
|
4040
|
+
}
|
|
4041
|
+
},
|
|
4042
|
+
{
|
|
4043
|
+
name: "check",
|
|
4044
|
+
description: "Check a checkbox matching the given CSS selector.",
|
|
4045
|
+
input_schema: {
|
|
4046
|
+
type: "object",
|
|
4047
|
+
properties: {
|
|
4048
|
+
selector: {
|
|
4049
|
+
type: "string",
|
|
4050
|
+
description: "CSS selector of the checkbox to check."
|
|
4051
|
+
}
|
|
4052
|
+
},
|
|
4053
|
+
required: ["selector"]
|
|
4054
|
+
}
|
|
4055
|
+
},
|
|
4056
|
+
{
|
|
4057
|
+
name: "uncheck",
|
|
4058
|
+
description: "Uncheck a checkbox matching the given CSS selector.",
|
|
4059
|
+
input_schema: {
|
|
4060
|
+
type: "object",
|
|
4061
|
+
properties: {
|
|
4062
|
+
selector: {
|
|
4063
|
+
type: "string",
|
|
4064
|
+
description: "CSS selector of the checkbox to uncheck."
|
|
4065
|
+
}
|
|
4066
|
+
},
|
|
4067
|
+
required: ["selector"]
|
|
4068
|
+
}
|
|
4069
|
+
},
|
|
4070
|
+
{
|
|
4071
|
+
name: "report_result",
|
|
4072
|
+
description: "Report the final test result. Call this when you have completed testing the scenario. This MUST be the last tool you call.",
|
|
4073
|
+
input_schema: {
|
|
4074
|
+
type: "object",
|
|
4075
|
+
properties: {
|
|
4076
|
+
status: {
|
|
4077
|
+
type: "string",
|
|
4078
|
+
enum: ["passed", "failed"],
|
|
4079
|
+
description: "Whether the test scenario passed or failed."
|
|
4080
|
+
},
|
|
4081
|
+
reasoning: {
|
|
4082
|
+
type: "string",
|
|
4083
|
+
description: "Detailed explanation of why the test passed or failed, including any issues found."
|
|
4084
|
+
}
|
|
4085
|
+
},
|
|
4086
|
+
required: ["status", "reasoning"]
|
|
4087
|
+
}
|
|
4088
|
+
}
|
|
4089
|
+
];
|
|
4090
|
+
});
|
|
4552
4091
|
|
|
4553
4092
|
// src/lib/config.ts
|
|
4554
|
-
init_types();
|
|
4555
4093
|
import { homedir as homedir3 } from "os";
|
|
4556
4094
|
import { join as join3 } from "path";
|
|
4557
4095
|
import { readFileSync, existsSync as existsSync3 } from "fs";
|
|
4558
|
-
var CONFIG_DIR = join3(homedir3(), ".testers");
|
|
4559
|
-
var CONFIG_PATH = join3(CONFIG_DIR, "config.json");
|
|
4560
4096
|
function getDefaultConfig() {
|
|
4561
4097
|
return {
|
|
4562
4098
|
defaultModel: "claude-haiku-4-5-20251001",
|
|
@@ -4605,9 +4141,14 @@ function loadConfig() {
|
|
|
4605
4141
|
}
|
|
4606
4142
|
return config;
|
|
4607
4143
|
}
|
|
4144
|
+
var CONFIG_DIR, CONFIG_PATH;
|
|
4145
|
+
var init_config = __esm(() => {
|
|
4146
|
+
init_types();
|
|
4147
|
+
CONFIG_DIR = join3(homedir3(), ".testers");
|
|
4148
|
+
CONFIG_PATH = join3(CONFIG_DIR, "config.json");
|
|
4149
|
+
});
|
|
4608
4150
|
|
|
4609
4151
|
// src/lib/webhooks.ts
|
|
4610
|
-
init_database();
|
|
4611
4152
|
function fromRow(row) {
|
|
4612
4153
|
return {
|
|
4613
4154
|
id: row.id,
|
|
@@ -4664,80 +4205,440 @@ Schedule: ${payload.schedule.name}` : "")
|
|
|
4664
4205
|
]
|
|
4665
4206
|
};
|
|
4666
4207
|
}
|
|
4667
|
-
async function dispatchWebhooks(event, run, schedule) {
|
|
4668
|
-
const webhooks = listWebhooks(run.projectId ?? undefined);
|
|
4669
|
-
const payload = {
|
|
4670
|
-
event,
|
|
4671
|
-
run: {
|
|
4672
|
-
id: run.id,
|
|
4673
|
-
url: run.url,
|
|
4674
|
-
status: run.status,
|
|
4675
|
-
passed: run.passed,
|
|
4676
|
-
failed: run.failed,
|
|
4677
|
-
total: run.total
|
|
4678
|
-
},
|
|
4679
|
-
schedule,
|
|
4680
|
-
timestamp: new Date().toISOString()
|
|
4681
|
-
};
|
|
4682
|
-
for (const webhook of webhooks) {
|
|
4683
|
-
if (!webhook.events.includes(event) && !webhook.events.includes("*"))
|
|
4208
|
+
async function dispatchWebhooks(event, run, schedule) {
|
|
4209
|
+
const webhooks = listWebhooks(run.projectId ?? undefined);
|
|
4210
|
+
const payload = {
|
|
4211
|
+
event,
|
|
4212
|
+
run: {
|
|
4213
|
+
id: run.id,
|
|
4214
|
+
url: run.url,
|
|
4215
|
+
status: run.status,
|
|
4216
|
+
passed: run.passed,
|
|
4217
|
+
failed: run.failed,
|
|
4218
|
+
total: run.total
|
|
4219
|
+
},
|
|
4220
|
+
schedule,
|
|
4221
|
+
timestamp: new Date().toISOString()
|
|
4222
|
+
};
|
|
4223
|
+
for (const webhook of webhooks) {
|
|
4224
|
+
if (!webhook.events.includes(event) && !webhook.events.includes("*"))
|
|
4225
|
+
continue;
|
|
4226
|
+
const isSlack = webhook.url.includes("hooks.slack.com");
|
|
4227
|
+
const body = isSlack ? JSON.stringify(formatSlackPayload(payload)) : JSON.stringify(payload);
|
|
4228
|
+
const headers = {
|
|
4229
|
+
"Content-Type": "application/json"
|
|
4230
|
+
};
|
|
4231
|
+
if (webhook.secret) {
|
|
4232
|
+
headers["X-Testers-Signature"] = signPayload(body, webhook.secret);
|
|
4233
|
+
}
|
|
4234
|
+
try {
|
|
4235
|
+
const response = await fetch(webhook.url, {
|
|
4236
|
+
method: "POST",
|
|
4237
|
+
headers,
|
|
4238
|
+
body
|
|
4239
|
+
});
|
|
4240
|
+
if (!response.ok) {
|
|
4241
|
+
await new Promise((r) => setTimeout(r, 5000));
|
|
4242
|
+
await fetch(webhook.url, { method: "POST", headers, body });
|
|
4243
|
+
}
|
|
4244
|
+
} catch {}
|
|
4245
|
+
}
|
|
4246
|
+
}
|
|
4247
|
+
var init_webhooks = __esm(() => {
|
|
4248
|
+
init_database();
|
|
4249
|
+
});
|
|
4250
|
+
|
|
4251
|
+
// src/lib/logs-integration.ts
|
|
4252
|
+
async function pushFailedRunToLogs(run, failedResults, scenarios) {
|
|
4253
|
+
const logsUrl = process.env.LOGS_URL;
|
|
4254
|
+
if (!logsUrl)
|
|
4255
|
+
return;
|
|
4256
|
+
const scenarioMap = new Map(scenarios.map((s) => [s.id, s]));
|
|
4257
|
+
const entries = failedResults.map((result) => {
|
|
4258
|
+
const scenario = scenarioMap.get(result.scenarioId);
|
|
4259
|
+
return {
|
|
4260
|
+
level: "error",
|
|
4261
|
+
source: "sdk",
|
|
4262
|
+
service: "testers",
|
|
4263
|
+
message: `[testers] Scenario failed: ${scenario?.name ?? result.scenarioId}${result.error ? ` \u2014 ${result.error}` : ""}`,
|
|
4264
|
+
metadata: {
|
|
4265
|
+
run_id: run.id,
|
|
4266
|
+
scenario_id: result.scenarioId,
|
|
4267
|
+
scenario_name: scenario?.name,
|
|
4268
|
+
url: run.url,
|
|
4269
|
+
status: result.status,
|
|
4270
|
+
duration_ms: result.durationMs
|
|
4271
|
+
}
|
|
4272
|
+
};
|
|
4273
|
+
});
|
|
4274
|
+
try {
|
|
4275
|
+
await fetch(`${logsUrl.replace(/\/$/, "")}/api/logs`, {
|
|
4276
|
+
method: "POST",
|
|
4277
|
+
headers: { "Content-Type": "application/json" },
|
|
4278
|
+
body: JSON.stringify(entries)
|
|
4279
|
+
});
|
|
4280
|
+
} catch {}
|
|
4281
|
+
}
|
|
4282
|
+
|
|
4283
|
+
// src/lib/todos-connector.ts
|
|
4284
|
+
import { Database as Database2 } from "bun:sqlite";
|
|
4285
|
+
import { existsSync as existsSync4 } from "fs";
|
|
4286
|
+
import { join as join4 } from "path";
|
|
4287
|
+
import { homedir as homedir4 } from "os";
|
|
4288
|
+
function resolveTodosDbPath() {
|
|
4289
|
+
const envPath = process.env["TODOS_DB_PATH"];
|
|
4290
|
+
if (envPath)
|
|
4291
|
+
return envPath;
|
|
4292
|
+
return join4(homedir4(), ".todos", "todos.db");
|
|
4293
|
+
}
|
|
4294
|
+
function connectToTodos() {
|
|
4295
|
+
const dbPath = resolveTodosDbPath();
|
|
4296
|
+
if (!existsSync4(dbPath)) {
|
|
4297
|
+
throw new TodosConnectionError(`Todos database not found at ${dbPath}. Install @hasna/todos or set TODOS_DB_PATH.`);
|
|
4298
|
+
}
|
|
4299
|
+
const db2 = new Database2(dbPath, { readonly: true });
|
|
4300
|
+
db2.exec("PRAGMA foreign_keys = ON");
|
|
4301
|
+
return db2;
|
|
4302
|
+
}
|
|
4303
|
+
function pullTasks(options = {}) {
|
|
4304
|
+
const db2 = connectToTodos();
|
|
4305
|
+
try {
|
|
4306
|
+
let query = "SELECT id, short_id, title, description, status, priority, tags, project_id FROM tasks WHERE 1=1";
|
|
4307
|
+
const params = [];
|
|
4308
|
+
if (options.status) {
|
|
4309
|
+
query += " AND status = ?";
|
|
4310
|
+
params.push(options.status);
|
|
4311
|
+
} else {
|
|
4312
|
+
query += " AND status IN ('pending', 'in_progress')";
|
|
4313
|
+
}
|
|
4314
|
+
if (options.priority) {
|
|
4315
|
+
query += " AND priority = ?";
|
|
4316
|
+
params.push(options.priority);
|
|
4317
|
+
}
|
|
4318
|
+
if (options.projectName) {
|
|
4319
|
+
const project = db2.query("SELECT id FROM projects WHERE name = ?").get(options.projectName);
|
|
4320
|
+
if (project) {
|
|
4321
|
+
query += " AND project_id = ?";
|
|
4322
|
+
params.push(project.id);
|
|
4323
|
+
}
|
|
4324
|
+
}
|
|
4325
|
+
query += " ORDER BY CASE priority WHEN 'critical' THEN 0 WHEN 'high' THEN 1 WHEN 'medium' THEN 2 WHEN 'low' THEN 3 END";
|
|
4326
|
+
const tasks = db2.query(query).all(...params);
|
|
4327
|
+
if (options.tags && options.tags.length > 0) {
|
|
4328
|
+
return tasks.filter((task) => {
|
|
4329
|
+
const taskTags = JSON.parse(task.tags || "[]");
|
|
4330
|
+
return options.tags.some((tag) => taskTags.includes(tag));
|
|
4331
|
+
});
|
|
4332
|
+
}
|
|
4333
|
+
return tasks;
|
|
4334
|
+
} finally {
|
|
4335
|
+
db2.close();
|
|
4336
|
+
}
|
|
4337
|
+
}
|
|
4338
|
+
function taskToScenarioInput(task, projectId) {
|
|
4339
|
+
const tags = JSON.parse(task.tags || "[]");
|
|
4340
|
+
const priority = ["low", "medium", "high", "critical"].includes(task.priority) ? task.priority : "medium";
|
|
4341
|
+
const steps = [];
|
|
4342
|
+
if (task.description) {
|
|
4343
|
+
const lines = task.description.split(`
|
|
4344
|
+
`);
|
|
4345
|
+
for (const line of lines) {
|
|
4346
|
+
const match = line.match(/^\s*\d+[\.\)]\s*(.+)/);
|
|
4347
|
+
if (match?.[1]) {
|
|
4348
|
+
steps.push(match[1].trim());
|
|
4349
|
+
}
|
|
4350
|
+
}
|
|
4351
|
+
}
|
|
4352
|
+
return {
|
|
4353
|
+
name: task.title.replace(/^(OPE\d+-\d+|[A-Z]+-\d+):\s*/, ""),
|
|
4354
|
+
description: task.description || task.title,
|
|
4355
|
+
steps,
|
|
4356
|
+
tags,
|
|
4357
|
+
priority,
|
|
4358
|
+
projectId,
|
|
4359
|
+
metadata: { todosTaskId: task.id, todosShortId: task.short_id }
|
|
4360
|
+
};
|
|
4361
|
+
}
|
|
4362
|
+
function importFromTodos(options = {}) {
|
|
4363
|
+
const tasks = pullTasks({
|
|
4364
|
+
projectName: options.projectName,
|
|
4365
|
+
tags: options.tags ?? ["qa", "test", "testing"],
|
|
4366
|
+
priority: options.priority
|
|
4367
|
+
});
|
|
4368
|
+
const existing = listScenarios({ projectId: options.projectId });
|
|
4369
|
+
const existingTodoIds = new Set(existing.filter((s) => s.metadata?.todosTaskId).map((s) => s.metadata.todosTaskId));
|
|
4370
|
+
let imported = 0;
|
|
4371
|
+
let skipped = 0;
|
|
4372
|
+
for (const task of tasks) {
|
|
4373
|
+
if (existingTodoIds.has(task.id)) {
|
|
4374
|
+
skipped++;
|
|
4684
4375
|
continue;
|
|
4685
|
-
const isSlack = webhook.url.includes("hooks.slack.com");
|
|
4686
|
-
const body = isSlack ? JSON.stringify(formatSlackPayload(payload)) : JSON.stringify(payload);
|
|
4687
|
-
const headers = {
|
|
4688
|
-
"Content-Type": "application/json"
|
|
4689
|
-
};
|
|
4690
|
-
if (webhook.secret) {
|
|
4691
|
-
headers["X-Testers-Signature"] = signPayload(body, webhook.secret);
|
|
4692
4376
|
}
|
|
4693
|
-
|
|
4694
|
-
|
|
4695
|
-
|
|
4696
|
-
headers,
|
|
4697
|
-
body
|
|
4698
|
-
});
|
|
4699
|
-
if (!response.ok) {
|
|
4700
|
-
await new Promise((r) => setTimeout(r, 5000));
|
|
4701
|
-
await fetch(webhook.url, { method: "POST", headers, body });
|
|
4702
|
-
}
|
|
4703
|
-
} catch {}
|
|
4377
|
+
const input = taskToScenarioInput(task, options.projectId);
|
|
4378
|
+
createScenario(input);
|
|
4379
|
+
imported++;
|
|
4704
4380
|
}
|
|
4381
|
+
return { imported, skipped };
|
|
4705
4382
|
}
|
|
4383
|
+
var init_todos_connector = __esm(() => {
|
|
4384
|
+
init_scenarios();
|
|
4385
|
+
init_types();
|
|
4386
|
+
});
|
|
4706
4387
|
|
|
4707
|
-
// src/lib/
|
|
4708
|
-
async function
|
|
4709
|
-
|
|
4710
|
-
|
|
4711
|
-
|
|
4388
|
+
// src/lib/failure-pipeline.ts
|
|
4389
|
+
async function createFailureTasks(run, failedResults, scenarios) {
|
|
4390
|
+
if (failedResults.length === 0)
|
|
4391
|
+
return { created: 0, skipped: 0 };
|
|
4392
|
+
const projectId = process.env["TESTERS_TODOS_PROJECT_ID"];
|
|
4393
|
+
if (!projectId)
|
|
4394
|
+
return { created: 0, skipped: 0 };
|
|
4395
|
+
let db2 = null;
|
|
4396
|
+
try {
|
|
4397
|
+
db2 = connectToTodos();
|
|
4398
|
+
} catch {
|
|
4399
|
+
return { created: 0, skipped: 0 };
|
|
4400
|
+
}
|
|
4712
4401
|
const scenarioMap = new Map(scenarios.map((s) => [s.id, s]));
|
|
4713
|
-
|
|
4714
|
-
|
|
4715
|
-
|
|
4716
|
-
|
|
4717
|
-
|
|
4718
|
-
|
|
4719
|
-
|
|
4720
|
-
|
|
4721
|
-
|
|
4722
|
-
|
|
4723
|
-
scenario_name: scenario?.name,
|
|
4724
|
-
url: run.url,
|
|
4725
|
-
status: result.status,
|
|
4726
|
-
duration_ms: result.durationMs
|
|
4402
|
+
let created = 0;
|
|
4403
|
+
let skipped = 0;
|
|
4404
|
+
try {
|
|
4405
|
+
for (const result of failedResults) {
|
|
4406
|
+
const scenario = scenarioMap.get(result.scenarioId);
|
|
4407
|
+
const title = `BUG: [testers] ${scenario?.name ?? result.scenarioId} failed`;
|
|
4408
|
+
const existing = db2.query("SELECT id FROM tasks WHERE title = ? AND status NOT IN ('completed', 'cancelled') LIMIT 1").get(title);
|
|
4409
|
+
if (existing) {
|
|
4410
|
+
skipped++;
|
|
4411
|
+
continue;
|
|
4727
4412
|
}
|
|
4728
|
-
|
|
4413
|
+
const id = crypto.randomUUID();
|
|
4414
|
+
const now2 = new Date().toISOString();
|
|
4415
|
+
const description = [
|
|
4416
|
+
`Test failure detected by open-testers.`,
|
|
4417
|
+
``,
|
|
4418
|
+
`**Run:** ${run.id}`,
|
|
4419
|
+
`**URL:** ${run.url}`,
|
|
4420
|
+
`**Scenario:** ${scenario?.name ?? result.scenarioId}`,
|
|
4421
|
+
`**Status:** ${result.status}`,
|
|
4422
|
+
result.error ? `**Error:** ${result.error}` : null,
|
|
4423
|
+
result.reasoning ? `**Reasoning:** ${result.reasoning.slice(0, 500)}` : null,
|
|
4424
|
+
`**Duration:** ${result.durationMs ? `${(result.durationMs / 1000).toFixed(1)}s` : "N/A"}`,
|
|
4425
|
+
`**Tokens:** ${result.tokensUsed ?? 0}`
|
|
4426
|
+
].filter(Boolean).join(`
|
|
4427
|
+
`);
|
|
4428
|
+
try {
|
|
4429
|
+
db2.query(`
|
|
4430
|
+
INSERT INTO tasks (id, short_id, title, description, status, priority, tags, project_id, version, created_at, updated_at)
|
|
4431
|
+
VALUES (?, ?, ?, ?, 'pending', 'high', ?, ?, 1, ?, ?)
|
|
4432
|
+
`).run(id, `BUG-${id.slice(0, 6)}`, title, description, JSON.stringify(["bug", "testers", "auto-created"]), projectId, now2, now2);
|
|
4433
|
+
created++;
|
|
4434
|
+
} catch {
|
|
4435
|
+
skipped++;
|
|
4436
|
+
}
|
|
4437
|
+
}
|
|
4438
|
+
} finally {
|
|
4439
|
+
db2.close();
|
|
4440
|
+
}
|
|
4441
|
+
return { created, skipped };
|
|
4442
|
+
}
|
|
4443
|
+
async function notifyFailureToConversations(run, failedResults, scenarios) {
|
|
4444
|
+
const baseUrl = process.env["TESTERS_CONVERSATIONS_URL"];
|
|
4445
|
+
const space = process.env["TESTERS_CONVERSATIONS_SPACE"];
|
|
4446
|
+
if (!baseUrl || !space)
|
|
4447
|
+
return;
|
|
4448
|
+
const scenarioMap = new Map(scenarios.map((s) => [s.id, s]));
|
|
4449
|
+
const total = run.total;
|
|
4450
|
+
const failedCount = failedResults.length;
|
|
4451
|
+
const passedCount = run.passed;
|
|
4452
|
+
const failureLines = failedResults.slice(0, 5).map((r) => {
|
|
4453
|
+
const name = scenarioMap.get(r.scenarioId)?.name ?? r.scenarioId;
|
|
4454
|
+
const err = r.error ? ` \u2014 ${r.error.slice(0, 120)}` : "";
|
|
4455
|
+
return ` \u274C ${name}${err}`;
|
|
4729
4456
|
});
|
|
4457
|
+
const extra = failedResults.length > 5 ? ` \u2026 and ${failedResults.length - 5} more` : "";
|
|
4458
|
+
const message = [
|
|
4459
|
+
`\uD83D\uDEA8 **Testers run failed** \u2014 ${failedCount}/${total} scenarios failed`,
|
|
4460
|
+
``,
|
|
4461
|
+
`**URL:** ${run.url}`,
|
|
4462
|
+
`**Run ID:** \`${run.id}\``,
|
|
4463
|
+
`**Pass rate:** ${passedCount}/${total}`,
|
|
4464
|
+
``,
|
|
4465
|
+
`**Failures:**`,
|
|
4466
|
+
...failureLines,
|
|
4467
|
+
extra
|
|
4468
|
+
].filter((l) => l !== "").join(`
|
|
4469
|
+
`);
|
|
4730
4470
|
try {
|
|
4731
|
-
await fetch(`${
|
|
4471
|
+
await fetch(`${baseUrl.replace(/\/$/, "")}/api/spaces/${encodeURIComponent(space)}/messages`, {
|
|
4732
4472
|
method: "POST",
|
|
4733
4473
|
headers: { "Content-Type": "application/json" },
|
|
4734
|
-
body: JSON.stringify(
|
|
4474
|
+
body: JSON.stringify({ content: message, from: "testers" })
|
|
4735
4475
|
});
|
|
4736
4476
|
} catch {}
|
|
4737
4477
|
}
|
|
4478
|
+
var init_failure_pipeline = __esm(() => {
|
|
4479
|
+
init_todos_connector();
|
|
4480
|
+
});
|
|
4481
|
+
|
|
4482
|
+
// src/db/flows.ts
|
|
4483
|
+
var exports_flows = {};
|
|
4484
|
+
__export(exports_flows, {
|
|
4485
|
+
topologicalSort: () => topologicalSort,
|
|
4486
|
+
removeDependency: () => removeDependency,
|
|
4487
|
+
listFlows: () => listFlows,
|
|
4488
|
+
getTransitiveDependencies: () => getTransitiveDependencies,
|
|
4489
|
+
getFlow: () => getFlow,
|
|
4490
|
+
getDependents: () => getDependents,
|
|
4491
|
+
getDependencies: () => getDependencies,
|
|
4492
|
+
deleteFlow: () => deleteFlow,
|
|
4493
|
+
createFlow: () => createFlow,
|
|
4494
|
+
addDependency: () => addDependency
|
|
4495
|
+
});
|
|
4496
|
+
function addDependency(scenarioId, dependsOn) {
|
|
4497
|
+
const db2 = getDatabase();
|
|
4498
|
+
const visited = new Set;
|
|
4499
|
+
const queue = [dependsOn];
|
|
4500
|
+
while (queue.length > 0) {
|
|
4501
|
+
const current = queue.shift();
|
|
4502
|
+
if (current === scenarioId) {
|
|
4503
|
+
throw new DependencyCycleError(scenarioId, dependsOn);
|
|
4504
|
+
}
|
|
4505
|
+
if (visited.has(current))
|
|
4506
|
+
continue;
|
|
4507
|
+
visited.add(current);
|
|
4508
|
+
const deps = db2.query("SELECT depends_on FROM scenario_dependencies WHERE scenario_id = ?").all(current);
|
|
4509
|
+
for (const dep of deps) {
|
|
4510
|
+
if (!visited.has(dep.depends_on)) {
|
|
4511
|
+
queue.push(dep.depends_on);
|
|
4512
|
+
}
|
|
4513
|
+
}
|
|
4514
|
+
}
|
|
4515
|
+
db2.query("INSERT OR IGNORE INTO scenario_dependencies (scenario_id, depends_on) VALUES (?, ?)").run(scenarioId, dependsOn);
|
|
4516
|
+
}
|
|
4517
|
+
function removeDependency(scenarioId, dependsOn) {
|
|
4518
|
+
const db2 = getDatabase();
|
|
4519
|
+
const result = db2.query("DELETE FROM scenario_dependencies WHERE scenario_id = ? AND depends_on = ?").run(scenarioId, dependsOn);
|
|
4520
|
+
return result.changes > 0;
|
|
4521
|
+
}
|
|
4522
|
+
function getDependencies(scenarioId) {
|
|
4523
|
+
const db2 = getDatabase();
|
|
4524
|
+
const rows = db2.query("SELECT depends_on FROM scenario_dependencies WHERE scenario_id = ?").all(scenarioId);
|
|
4525
|
+
return rows.map((r) => r.depends_on);
|
|
4526
|
+
}
|
|
4527
|
+
function getDependents(scenarioId) {
|
|
4528
|
+
const db2 = getDatabase();
|
|
4529
|
+
const rows = db2.query("SELECT scenario_id FROM scenario_dependencies WHERE depends_on = ?").all(scenarioId);
|
|
4530
|
+
return rows.map((r) => r.scenario_id);
|
|
4531
|
+
}
|
|
4532
|
+
function getTransitiveDependencies(scenarioId) {
|
|
4533
|
+
const db2 = getDatabase();
|
|
4534
|
+
const visited = new Set;
|
|
4535
|
+
const queue = [scenarioId];
|
|
4536
|
+
while (queue.length > 0) {
|
|
4537
|
+
const current = queue.shift();
|
|
4538
|
+
const deps = db2.query("SELECT depends_on FROM scenario_dependencies WHERE scenario_id = ?").all(current);
|
|
4539
|
+
for (const dep of deps) {
|
|
4540
|
+
if (!visited.has(dep.depends_on)) {
|
|
4541
|
+
visited.add(dep.depends_on);
|
|
4542
|
+
queue.push(dep.depends_on);
|
|
4543
|
+
}
|
|
4544
|
+
}
|
|
4545
|
+
}
|
|
4546
|
+
return Array.from(visited);
|
|
4547
|
+
}
|
|
4548
|
+
function topologicalSort(scenarioIds) {
|
|
4549
|
+
const db2 = getDatabase();
|
|
4550
|
+
const idSet = new Set(scenarioIds);
|
|
4551
|
+
const inDegree = new Map;
|
|
4552
|
+
const dependents = new Map;
|
|
4553
|
+
for (const id of scenarioIds) {
|
|
4554
|
+
inDegree.set(id, 0);
|
|
4555
|
+
dependents.set(id, []);
|
|
4556
|
+
}
|
|
4557
|
+
for (const id of scenarioIds) {
|
|
4558
|
+
const deps = db2.query("SELECT depends_on FROM scenario_dependencies WHERE scenario_id = ?").all(id);
|
|
4559
|
+
for (const dep of deps) {
|
|
4560
|
+
if (idSet.has(dep.depends_on)) {
|
|
4561
|
+
inDegree.set(id, (inDegree.get(id) ?? 0) + 1);
|
|
4562
|
+
dependents.get(dep.depends_on).push(id);
|
|
4563
|
+
}
|
|
4564
|
+
}
|
|
4565
|
+
}
|
|
4566
|
+
const queue = [];
|
|
4567
|
+
for (const [id, deg] of inDegree) {
|
|
4568
|
+
if (deg === 0)
|
|
4569
|
+
queue.push(id);
|
|
4570
|
+
}
|
|
4571
|
+
const sorted = [];
|
|
4572
|
+
while (queue.length > 0) {
|
|
4573
|
+
const current = queue.shift();
|
|
4574
|
+
sorted.push(current);
|
|
4575
|
+
for (const dep of dependents.get(current) ?? []) {
|
|
4576
|
+
const newDeg = (inDegree.get(dep) ?? 1) - 1;
|
|
4577
|
+
inDegree.set(dep, newDeg);
|
|
4578
|
+
if (newDeg === 0)
|
|
4579
|
+
queue.push(dep);
|
|
4580
|
+
}
|
|
4581
|
+
}
|
|
4582
|
+
if (sorted.length !== scenarioIds.length) {
|
|
4583
|
+
throw new DependencyCycleError("multiple", "multiple");
|
|
4584
|
+
}
|
|
4585
|
+
return sorted;
|
|
4586
|
+
}
|
|
4587
|
+
function createFlow(input) {
|
|
4588
|
+
const db2 = getDatabase();
|
|
4589
|
+
const id = uuid();
|
|
4590
|
+
const timestamp = now();
|
|
4591
|
+
db2.query(`
|
|
4592
|
+
INSERT INTO flows (id, project_id, name, description, scenario_ids, created_at, updated_at)
|
|
4593
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
4594
|
+
`).run(id, input.projectId ?? null, input.name, input.description ?? null, JSON.stringify(input.scenarioIds), timestamp, timestamp);
|
|
4595
|
+
return getFlow(id);
|
|
4596
|
+
}
|
|
4597
|
+
function getFlow(id) {
|
|
4598
|
+
const db2 = getDatabase();
|
|
4599
|
+
let row = db2.query("SELECT * FROM flows WHERE id = ?").get(id);
|
|
4600
|
+
if (row)
|
|
4601
|
+
return flowFromRow(row);
|
|
4602
|
+
const fullId = resolvePartialId("flows", id);
|
|
4603
|
+
if (fullId) {
|
|
4604
|
+
row = db2.query("SELECT * FROM flows WHERE id = ?").get(fullId);
|
|
4605
|
+
if (row)
|
|
4606
|
+
return flowFromRow(row);
|
|
4607
|
+
}
|
|
4608
|
+
return null;
|
|
4609
|
+
}
|
|
4610
|
+
function listFlows(projectId) {
|
|
4611
|
+
const db2 = getDatabase();
|
|
4612
|
+
if (projectId) {
|
|
4613
|
+
const rows2 = db2.query("SELECT * FROM flows WHERE project_id = ? ORDER BY created_at DESC").all(projectId);
|
|
4614
|
+
return rows2.map(flowFromRow);
|
|
4615
|
+
}
|
|
4616
|
+
const rows = db2.query("SELECT * FROM flows ORDER BY created_at DESC").all();
|
|
4617
|
+
return rows.map(flowFromRow);
|
|
4618
|
+
}
|
|
4619
|
+
function deleteFlow(id) {
|
|
4620
|
+
const db2 = getDatabase();
|
|
4621
|
+
const flow = getFlow(id);
|
|
4622
|
+
if (!flow)
|
|
4623
|
+
return false;
|
|
4624
|
+
const result = db2.query("DELETE FROM flows WHERE id = ?").run(flow.id);
|
|
4625
|
+
return result.changes > 0;
|
|
4626
|
+
}
|
|
4627
|
+
var init_flows = __esm(() => {
|
|
4628
|
+
init_database();
|
|
4629
|
+
init_database();
|
|
4630
|
+
init_types();
|
|
4631
|
+
});
|
|
4738
4632
|
|
|
4739
4633
|
// src/lib/runner.ts
|
|
4740
|
-
var
|
|
4634
|
+
var exports_runner = {};
|
|
4635
|
+
__export(exports_runner, {
|
|
4636
|
+
startRunAsync: () => startRunAsync,
|
|
4637
|
+
runSingleScenario: () => runSingleScenario,
|
|
4638
|
+
runByFilter: () => runByFilter,
|
|
4639
|
+
runBatch: () => runBatch,
|
|
4640
|
+
onRunEvent: () => onRunEvent
|
|
4641
|
+
});
|
|
4741
4642
|
function onRunEvent(handler) {
|
|
4742
4643
|
eventHandler = handler;
|
|
4743
4644
|
}
|
|
@@ -4972,6 +4873,8 @@ async function runBatch(scenarios, options) {
|
|
|
4972
4873
|
if (finalRun.status === "failed") {
|
|
4973
4874
|
const failedResults = results.filter((r) => r.status === "failed" || r.status === "error");
|
|
4974
4875
|
pushFailedRunToLogs(finalRun, failedResults, scenarios).catch(() => {});
|
|
4876
|
+
createFailureTasks(finalRun, failedResults, scenarios).catch(() => {});
|
|
4877
|
+
notifyFailureToConversations(finalRun, failedResults, scenarios).catch(() => {});
|
|
4975
4878
|
}
|
|
4976
4879
|
return { run: finalRun, results };
|
|
4977
4880
|
}
|
|
@@ -5084,11 +4987,34 @@ function estimateCost(model, tokens) {
|
|
|
5084
4987
|
const costPer1M = costs[model] ?? 0.5;
|
|
5085
4988
|
return tokens / 1e6 * costPer1M * 100;
|
|
5086
4989
|
}
|
|
4990
|
+
var eventHandler = null;
|
|
4991
|
+
var init_runner = __esm(() => {
|
|
4992
|
+
init_runs();
|
|
4993
|
+
init_results();
|
|
4994
|
+
init_screenshots();
|
|
4995
|
+
init_scenarios();
|
|
4996
|
+
init_browser();
|
|
4997
|
+
init_screenshotter();
|
|
4998
|
+
init_ai_client();
|
|
4999
|
+
init_config();
|
|
5000
|
+
init_webhooks();
|
|
5001
|
+
init_failure_pipeline();
|
|
5002
|
+
});
|
|
5087
5003
|
|
|
5088
5004
|
// src/lib/reporter.ts
|
|
5005
|
+
var exports_reporter = {};
|
|
5006
|
+
__export(exports_reporter, {
|
|
5007
|
+
getScenarioRunStats: () => getScenarioRunStats,
|
|
5008
|
+
getExitCode: () => getExitCode,
|
|
5009
|
+
formatTerminal: () => formatTerminal,
|
|
5010
|
+
formatSummary: () => formatSummary,
|
|
5011
|
+
formatScenarioList: () => formatScenarioList,
|
|
5012
|
+
formatRunList: () => formatRunList,
|
|
5013
|
+
formatResultDetail: () => formatResultDetail,
|
|
5014
|
+
formatJSON: () => formatJSON,
|
|
5015
|
+
formatActionableSummary: () => formatActionableSummary
|
|
5016
|
+
});
|
|
5089
5017
|
import chalk from "chalk";
|
|
5090
|
-
init_scenarios();
|
|
5091
|
-
init_database();
|
|
5092
5018
|
function useEmoji() {
|
|
5093
5019
|
return !process.env["NO_COLOR"] && process.argv.indexOf("--no-color") === -1;
|
|
5094
5020
|
}
|
|
@@ -5150,6 +5076,13 @@ function formatTerminal(run, results, options) {
|
|
|
5150
5076
|
return lines.join(`
|
|
5151
5077
|
`);
|
|
5152
5078
|
}
|
|
5079
|
+
function formatSummary(run) {
|
|
5080
|
+
const duration = run.finishedAt ? `${((new Date(run.finishedAt).getTime() - new Date(run.startedAt).getTime()) / 1000).toFixed(1)}s` : "running";
|
|
5081
|
+
const passedStr = chalk.green(`${run.passed} passed`);
|
|
5082
|
+
const failedStr = run.failed > 0 ? chalk.red(` ${run.failed} failed`) : "";
|
|
5083
|
+
const totalStr = chalk.dim(` (${run.total} total)`);
|
|
5084
|
+
return ` ${passedStr}${failedStr}${totalStr} in ${duration}`;
|
|
5085
|
+
}
|
|
5153
5086
|
function formatActionableSummary(run, results) {
|
|
5154
5087
|
const emoji = useEmoji();
|
|
5155
5088
|
const passedCount = results.filter((r) => r.status === "passed").length;
|
|
@@ -5222,182 +5155,707 @@ function formatJSON(run, results) {
|
|
|
5222
5155
|
durationMs: run.finishedAt ? new Date(run.finishedAt).getTime() - new Date(run.startedAt).getTime() : null
|
|
5223
5156
|
}
|
|
5224
5157
|
};
|
|
5225
|
-
return JSON.stringify(output, null, 2);
|
|
5226
|
-
}
|
|
5227
|
-
function getExitCode(run) {
|
|
5228
|
-
if (run.status === "passed")
|
|
5229
|
-
return 0;
|
|
5230
|
-
if (run.status === "failed")
|
|
5231
|
-
return 1;
|
|
5232
|
-
return 2;
|
|
5158
|
+
return JSON.stringify(output, null, 2);
|
|
5159
|
+
}
|
|
5160
|
+
function getExitCode(run) {
|
|
5161
|
+
if (run.status === "passed")
|
|
5162
|
+
return 0;
|
|
5163
|
+
if (run.status === "failed")
|
|
5164
|
+
return 1;
|
|
5165
|
+
return 2;
|
|
5166
|
+
}
|
|
5167
|
+
function formatRunList(runs) {
|
|
5168
|
+
const lines = [];
|
|
5169
|
+
lines.push("");
|
|
5170
|
+
lines.push(chalk.bold(" Recent Runs"));
|
|
5171
|
+
lines.push("");
|
|
5172
|
+
if (runs.length === 0) {
|
|
5173
|
+
lines.push(chalk.dim(" No runs found."));
|
|
5174
|
+
lines.push("");
|
|
5175
|
+
return lines.join(`
|
|
5176
|
+
`);
|
|
5177
|
+
}
|
|
5178
|
+
for (const run of runs) {
|
|
5179
|
+
const statusIcon = run.status === "passed" ? chalk.green("PASS") : run.status === "failed" ? chalk.red("FAIL") : run.status === "running" ? chalk.blue("RUN ") : chalk.dim(run.status.toUpperCase().padEnd(4));
|
|
5180
|
+
const date = new Date(run.startedAt).toLocaleString();
|
|
5181
|
+
const id = run.id.slice(0, 8);
|
|
5182
|
+
lines.push(` ${statusIcon} ${chalk.dim(id)} ${run.url} ${chalk.dim(`${run.passed}/${run.total}`)} ${chalk.dim(date)}`);
|
|
5183
|
+
}
|
|
5184
|
+
lines.push("");
|
|
5185
|
+
return lines.join(`
|
|
5186
|
+
`);
|
|
5187
|
+
}
|
|
5188
|
+
function getScenarioRunStats(scenarioId) {
|
|
5189
|
+
const db2 = getDatabase();
|
|
5190
|
+
const lastRow = db2.query("SELECT status FROM results WHERE scenario_id = ? ORDER BY created_at DESC LIMIT 1").get(scenarioId);
|
|
5191
|
+
const statsRow = db2.query("SELECT COUNT(*) as total, SUM(CASE WHEN status = 'passed' THEN 1 ELSE 0 END) as passed FROM results WHERE scenario_id = ?").get(scenarioId);
|
|
5192
|
+
return {
|
|
5193
|
+
lastStatus: lastRow ? lastRow.status : null,
|
|
5194
|
+
passRate: statsRow && statsRow.total > 0 ? `${statsRow.passed}/${statsRow.total}` : "\u2014"
|
|
5195
|
+
};
|
|
5196
|
+
}
|
|
5197
|
+
function formatScenarioList(scenarios) {
|
|
5198
|
+
const lines = [];
|
|
5199
|
+
lines.push("");
|
|
5200
|
+
lines.push(chalk.bold(" Scenarios"));
|
|
5201
|
+
lines.push("");
|
|
5202
|
+
if (scenarios.length === 0) {
|
|
5203
|
+
lines.push(chalk.dim(" No scenarios found. Use 'testers add' to create one."));
|
|
5204
|
+
lines.push("");
|
|
5205
|
+
return lines.join(`
|
|
5206
|
+
`);
|
|
5207
|
+
}
|
|
5208
|
+
for (const s of scenarios) {
|
|
5209
|
+
const priorityColor = s.priority === "critical" ? chalk.red : s.priority === "high" ? chalk.yellow : s.priority === "medium" ? chalk.blue : chalk.dim;
|
|
5210
|
+
const tags = s.tags.length > 0 ? chalk.dim(` [${s.tags.join(", ")}]`) : "";
|
|
5211
|
+
let lastStatusIcon = chalk.dim("\u2014");
|
|
5212
|
+
let passRateStr = chalk.dim("\u2014");
|
|
5213
|
+
if (s.id) {
|
|
5214
|
+
const stats = getScenarioRunStats(s.id);
|
|
5215
|
+
if (stats.lastStatus === "passed")
|
|
5216
|
+
lastStatusIcon = chalk.green("\u2713");
|
|
5217
|
+
else if (stats.lastStatus === "failed")
|
|
5218
|
+
lastStatusIcon = chalk.red("\u2717");
|
|
5219
|
+
else if (stats.lastStatus === "error")
|
|
5220
|
+
lastStatusIcon = chalk.yellow("!");
|
|
5221
|
+
else if (stats.lastStatus === "skipped")
|
|
5222
|
+
lastStatusIcon = chalk.dim("~");
|
|
5223
|
+
passRateStr = stats.passRate === "\u2014" ? chalk.dim("\u2014") : chalk.dim(stats.passRate);
|
|
5224
|
+
}
|
|
5225
|
+
lines.push(` ${chalk.cyan(s.shortId)} ${s.name} ${priorityColor(s.priority)}${tags} ${lastStatusIcon} ${passRateStr}`);
|
|
5226
|
+
}
|
|
5227
|
+
lines.push("");
|
|
5228
|
+
return lines.join(`
|
|
5229
|
+
`);
|
|
5230
|
+
}
|
|
5231
|
+
function formatResultDetail(result, screenshots) {
|
|
5232
|
+
const lines = [];
|
|
5233
|
+
const scenario = getScenario(result.scenarioId);
|
|
5234
|
+
lines.push("");
|
|
5235
|
+
lines.push(chalk.bold(` Result ${result.id.slice(0, 8)}`));
|
|
5236
|
+
if (scenario) {
|
|
5237
|
+
lines.push(` Scenario: ${scenario.shortId} \u2014 ${scenario.name}`);
|
|
5238
|
+
}
|
|
5239
|
+
lines.push(` Status: ${result.status === "passed" ? chalk.green("PASSED") : chalk.red(result.status.toUpperCase())}`);
|
|
5240
|
+
lines.push(` Model: ${result.model}`);
|
|
5241
|
+
lines.push(` Duration: ${(result.durationMs / 1000).toFixed(1)}s`);
|
|
5242
|
+
lines.push(` Steps: ${result.stepsCompleted}/${result.stepsTotal}`);
|
|
5243
|
+
lines.push(` Tokens: ${result.tokensUsed} (~$${(result.costCents / 100).toFixed(4)})`);
|
|
5244
|
+
if (result.reasoning) {
|
|
5245
|
+
lines.push("");
|
|
5246
|
+
lines.push(chalk.bold(" Reasoning:"));
|
|
5247
|
+
lines.push(` ${result.reasoning}`);
|
|
5248
|
+
}
|
|
5249
|
+
if (result.error) {
|
|
5250
|
+
lines.push("");
|
|
5251
|
+
lines.push(chalk.red.bold(" Error:"));
|
|
5252
|
+
lines.push(chalk.red(` ${result.error}`));
|
|
5253
|
+
}
|
|
5254
|
+
if (screenshots.length > 0) {
|
|
5255
|
+
lines.push("");
|
|
5256
|
+
lines.push(chalk.bold(` Screenshots (${screenshots.length}):`));
|
|
5257
|
+
for (const ss of screenshots) {
|
|
5258
|
+
lines.push(` ${chalk.dim(`${String(ss.stepNumber).padStart(3, "0")}`)} ${ss.action} \u2014 ${chalk.dim(ss.filePath)}`);
|
|
5259
|
+
}
|
|
5260
|
+
}
|
|
5261
|
+
lines.push("");
|
|
5262
|
+
return lines.join(`
|
|
5263
|
+
`);
|
|
5264
|
+
}
|
|
5265
|
+
var init_reporter = __esm(() => {
|
|
5266
|
+
init_screenshots();
|
|
5267
|
+
init_scenarios();
|
|
5268
|
+
init_database();
|
|
5269
|
+
});
|
|
5270
|
+
|
|
5271
|
+
// src/lib/openapi-import.ts
|
|
5272
|
+
var exports_openapi_import = {};
|
|
5273
|
+
__export(exports_openapi_import, {
|
|
5274
|
+
parseOpenAPISpec: () => parseOpenAPISpec,
|
|
5275
|
+
importFromOpenAPI: () => importFromOpenAPI
|
|
5276
|
+
});
|
|
5277
|
+
import { readFileSync as readFileSync5 } from "fs";
|
|
5278
|
+
function parseSpec(content) {
|
|
5279
|
+
try {
|
|
5280
|
+
return JSON.parse(content);
|
|
5281
|
+
} catch {
|
|
5282
|
+
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`");
|
|
5283
|
+
}
|
|
5284
|
+
}
|
|
5285
|
+
function methodPriority(method) {
|
|
5286
|
+
switch (method.toUpperCase()) {
|
|
5287
|
+
case "GET":
|
|
5288
|
+
return "medium";
|
|
5289
|
+
case "POST":
|
|
5290
|
+
return "high";
|
|
5291
|
+
case "PUT":
|
|
5292
|
+
return "high";
|
|
5293
|
+
case "DELETE":
|
|
5294
|
+
return "critical";
|
|
5295
|
+
case "PATCH":
|
|
5296
|
+
return "medium";
|
|
5297
|
+
default:
|
|
5298
|
+
return "low";
|
|
5299
|
+
}
|
|
5300
|
+
}
|
|
5301
|
+
function parseOpenAPISpec(filePathOrUrl) {
|
|
5302
|
+
let content;
|
|
5303
|
+
if (filePathOrUrl.startsWith("http")) {
|
|
5304
|
+
throw new Error("URL fetching not supported yet. Download the spec file first.");
|
|
5305
|
+
}
|
|
5306
|
+
content = readFileSync5(filePathOrUrl, "utf-8");
|
|
5307
|
+
const spec = parseSpec(content);
|
|
5308
|
+
const isOpenAPI3 = !!spec.openapi;
|
|
5309
|
+
const isSwagger2 = !!spec.swagger;
|
|
5310
|
+
if (!isOpenAPI3 && !isSwagger2) {
|
|
5311
|
+
throw new Error("Not a valid OpenAPI 3.x or Swagger 2.0 spec");
|
|
5312
|
+
}
|
|
5313
|
+
const scenarios = [];
|
|
5314
|
+
const paths = spec.paths ?? {};
|
|
5315
|
+
for (const [path, methods] of Object.entries(paths)) {
|
|
5316
|
+
for (const [method, operation] of Object.entries(methods)) {
|
|
5317
|
+
if (["get", "post", "put", "delete", "patch"].indexOf(method.toLowerCase()) === -1)
|
|
5318
|
+
continue;
|
|
5319
|
+
const op = operation;
|
|
5320
|
+
const name = op.summary ?? op.operationId ?? `${method.toUpperCase()} ${path}`;
|
|
5321
|
+
const tags = op.tags ?? [];
|
|
5322
|
+
const requiresAuth = !!(op.security?.length ?? spec.security?.length);
|
|
5323
|
+
const steps = [];
|
|
5324
|
+
steps.push(`Navigate to the API endpoint: ${method.toUpperCase()} ${path}`);
|
|
5325
|
+
if (op.parameters?.length) {
|
|
5326
|
+
const required = op.parameters.filter((p) => p.required);
|
|
5327
|
+
if (required.length > 0) {
|
|
5328
|
+
steps.push(`Fill required parameters: ${required.map((p) => p.name).join(", ")}`);
|
|
5329
|
+
}
|
|
5330
|
+
}
|
|
5331
|
+
if (["post", "put", "patch"].includes(method.toLowerCase())) {
|
|
5332
|
+
steps.push("Fill the request body with valid test data");
|
|
5333
|
+
}
|
|
5334
|
+
steps.push("Submit the request");
|
|
5335
|
+
const responses = op.responses ?? {};
|
|
5336
|
+
const successCodes = Object.keys(responses).filter((c) => c.startsWith("2"));
|
|
5337
|
+
if (successCodes.length > 0) {
|
|
5338
|
+
steps.push(`Verify response status is ${successCodes.join(" or ")}`);
|
|
5339
|
+
} else {
|
|
5340
|
+
steps.push("Verify the response is successful");
|
|
5341
|
+
}
|
|
5342
|
+
const description = [
|
|
5343
|
+
op.description ?? `Test the ${method.toUpperCase()} ${path} endpoint.`,
|
|
5344
|
+
requiresAuth ? "This endpoint requires authentication." : ""
|
|
5345
|
+
].filter(Boolean).join(" ");
|
|
5346
|
+
scenarios.push({
|
|
5347
|
+
name,
|
|
5348
|
+
description,
|
|
5349
|
+
steps,
|
|
5350
|
+
tags: [...tags, "api", method.toLowerCase()],
|
|
5351
|
+
priority: methodPriority(method),
|
|
5352
|
+
targetPath: path,
|
|
5353
|
+
requiresAuth
|
|
5354
|
+
});
|
|
5355
|
+
}
|
|
5356
|
+
}
|
|
5357
|
+
return scenarios;
|
|
5358
|
+
}
|
|
5359
|
+
function importFromOpenAPI(filePathOrUrl, projectId) {
|
|
5360
|
+
const inputs = parseOpenAPISpec(filePathOrUrl);
|
|
5361
|
+
const scenarios = inputs.map((input) => createScenario({ ...input, projectId }));
|
|
5362
|
+
return { imported: scenarios.length, scenarios };
|
|
5363
|
+
}
|
|
5364
|
+
var init_openapi_import = __esm(() => {
|
|
5365
|
+
init_scenarios();
|
|
5366
|
+
});
|
|
5367
|
+
|
|
5368
|
+
// src/lib/recorder.ts
|
|
5369
|
+
var exports_recorder = {};
|
|
5370
|
+
__export(exports_recorder, {
|
|
5371
|
+
recordSession: () => recordSession,
|
|
5372
|
+
recordAndSave: () => recordAndSave,
|
|
5373
|
+
actionsToScenarioInput: () => actionsToScenarioInput
|
|
5374
|
+
});
|
|
5375
|
+
import { chromium as chromium3 } from "playwright";
|
|
5376
|
+
async function recordSession(url, options) {
|
|
5377
|
+
const browser = await chromium3.launch({ headless: false });
|
|
5378
|
+
const context = await browser.newContext({ viewport: { width: 1280, height: 720 } });
|
|
5379
|
+
const page = await context.newPage();
|
|
5380
|
+
const actions = [];
|
|
5381
|
+
const startTime = Date.now();
|
|
5382
|
+
const timeout = options?.timeout ?? 300000;
|
|
5383
|
+
page.on("framenavigated", (frame) => {
|
|
5384
|
+
if (frame === page.mainFrame()) {
|
|
5385
|
+
actions.push({ type: "navigate", url: frame.url(), timestamp: Date.now() - startTime });
|
|
5386
|
+
}
|
|
5387
|
+
});
|
|
5388
|
+
await page.addInitScript(() => {
|
|
5389
|
+
document.addEventListener("click", (e) => {
|
|
5390
|
+
const target = e.target;
|
|
5391
|
+
const selector = buildSelector(target);
|
|
5392
|
+
window.postMessage({ __testers_action: "click", selector }, "*");
|
|
5393
|
+
}, true);
|
|
5394
|
+
document.addEventListener("input", (e) => {
|
|
5395
|
+
const target = e.target;
|
|
5396
|
+
const selector = buildSelector(target);
|
|
5397
|
+
window.postMessage({ __testers_action: "fill", selector, value: target.value }, "*");
|
|
5398
|
+
}, true);
|
|
5399
|
+
document.addEventListener("change", (e) => {
|
|
5400
|
+
const target = e.target;
|
|
5401
|
+
if (target.tagName === "SELECT") {
|
|
5402
|
+
const selector = buildSelector(target);
|
|
5403
|
+
window.postMessage({ __testers_action: "select", selector, value: target.value }, "*");
|
|
5404
|
+
}
|
|
5405
|
+
}, true);
|
|
5406
|
+
document.addEventListener("keydown", (e) => {
|
|
5407
|
+
if (["Enter", "Tab", "Escape"].includes(e.key)) {
|
|
5408
|
+
window.postMessage({ __testers_action: "press", key: e.key }, "*");
|
|
5409
|
+
}
|
|
5410
|
+
}, true);
|
|
5411
|
+
function buildSelector(el) {
|
|
5412
|
+
if (el.id)
|
|
5413
|
+
return `#${el.id}`;
|
|
5414
|
+
if (el.getAttribute("data-testid"))
|
|
5415
|
+
return `[data-testid="${el.getAttribute("data-testid")}"]`;
|
|
5416
|
+
if (el.getAttribute("name"))
|
|
5417
|
+
return `${el.tagName.toLowerCase()}[name="${el.getAttribute("name")}"]`;
|
|
5418
|
+
if (el.getAttribute("aria-label"))
|
|
5419
|
+
return `[aria-label="${el.getAttribute("aria-label")}"]`;
|
|
5420
|
+
if (el.className && typeof el.className === "string") {
|
|
5421
|
+
const classes = el.className.trim().split(/\s+/).slice(0, 2).join(".");
|
|
5422
|
+
if (classes)
|
|
5423
|
+
return `${el.tagName.toLowerCase()}.${classes}`;
|
|
5424
|
+
}
|
|
5425
|
+
const text = el.textContent?.trim().slice(0, 30);
|
|
5426
|
+
if (text)
|
|
5427
|
+
return `text="${text}"`;
|
|
5428
|
+
return el.tagName.toLowerCase();
|
|
5429
|
+
}
|
|
5430
|
+
});
|
|
5431
|
+
const pollInterval = setInterval(async () => {
|
|
5432
|
+
try {
|
|
5433
|
+
const newActions = await page.evaluate(() => {
|
|
5434
|
+
const collected = window.__testers_collected ?? [];
|
|
5435
|
+
window.__testers_collected = [];
|
|
5436
|
+
return collected;
|
|
5437
|
+
});
|
|
5438
|
+
for (const a of newActions) {
|
|
5439
|
+
actions.push({
|
|
5440
|
+
type: a["type"],
|
|
5441
|
+
selector: a["selector"],
|
|
5442
|
+
value: a["value"],
|
|
5443
|
+
key: a["key"],
|
|
5444
|
+
timestamp: Date.now() - startTime
|
|
5445
|
+
});
|
|
5446
|
+
}
|
|
5447
|
+
} catch {}
|
|
5448
|
+
}, 500);
|
|
5449
|
+
await page.exposeFunction("__testersRecord", (action) => {
|
|
5450
|
+
actions.push({ ...action, timestamp: Date.now() - startTime });
|
|
5451
|
+
});
|
|
5452
|
+
await page.addInitScript(() => {
|
|
5453
|
+
window.addEventListener("message", (e) => {
|
|
5454
|
+
if (e.data?.__testers_action) {
|
|
5455
|
+
const { __testers_action, ...rest } = e.data;
|
|
5456
|
+
window.__testersRecord({ type: __testers_action, ...rest });
|
|
5457
|
+
}
|
|
5458
|
+
});
|
|
5459
|
+
});
|
|
5460
|
+
await page.goto(url);
|
|
5461
|
+
actions.push({ type: "navigate", url, timestamp: 0 });
|
|
5462
|
+
console.log(`
|
|
5463
|
+
Recording started. Interact with the browser.`);
|
|
5464
|
+
console.log(` Close the browser window or wait ${timeout / 1000}s to stop.
|
|
5465
|
+
`);
|
|
5466
|
+
await Promise.race([
|
|
5467
|
+
page.waitForEvent("close").catch(() => {}),
|
|
5468
|
+
context.waitForEvent("close").catch(() => {}),
|
|
5469
|
+
new Promise((resolve) => setTimeout(resolve, timeout))
|
|
5470
|
+
]);
|
|
5471
|
+
clearInterval(pollInterval);
|
|
5472
|
+
try {
|
|
5473
|
+
await browser.close();
|
|
5474
|
+
} catch {}
|
|
5475
|
+
return {
|
|
5476
|
+
actions,
|
|
5477
|
+
url,
|
|
5478
|
+
duration: Date.now() - startTime
|
|
5479
|
+
};
|
|
5233
5480
|
}
|
|
5234
|
-
function
|
|
5235
|
-
const
|
|
5236
|
-
|
|
5237
|
-
|
|
5238
|
-
|
|
5239
|
-
|
|
5240
|
-
|
|
5241
|
-
|
|
5242
|
-
|
|
5243
|
-
|
|
5481
|
+
function actionsToScenarioInput(recording, name, projectId) {
|
|
5482
|
+
const steps = [];
|
|
5483
|
+
const seenFills = new Map;
|
|
5484
|
+
for (const action of recording.actions) {
|
|
5485
|
+
switch (action.type) {
|
|
5486
|
+
case "navigate":
|
|
5487
|
+
if (action.url)
|
|
5488
|
+
steps.push(`Navigate to ${action.url}`);
|
|
5489
|
+
break;
|
|
5490
|
+
case "click":
|
|
5491
|
+
if (action.selector)
|
|
5492
|
+
steps.push(`Click ${action.selector}`);
|
|
5493
|
+
break;
|
|
5494
|
+
case "fill":
|
|
5495
|
+
if (action.selector && action.value) {
|
|
5496
|
+
seenFills.set(action.selector, action.value);
|
|
5497
|
+
}
|
|
5498
|
+
break;
|
|
5499
|
+
case "select":
|
|
5500
|
+
if (action.selector && action.value)
|
|
5501
|
+
steps.push(`Select "${action.value}" in ${action.selector}`);
|
|
5502
|
+
break;
|
|
5503
|
+
case "press":
|
|
5504
|
+
if (action.key)
|
|
5505
|
+
steps.push(`Press ${action.key}`);
|
|
5506
|
+
break;
|
|
5507
|
+
}
|
|
5244
5508
|
}
|
|
5245
|
-
for (const
|
|
5246
|
-
|
|
5247
|
-
const date = new Date(run.startedAt).toLocaleString();
|
|
5248
|
-
const id = run.id.slice(0, 8);
|
|
5249
|
-
lines.push(` ${statusIcon} ${chalk.dim(id)} ${run.url} ${chalk.dim(`${run.passed}/${run.total}`)} ${chalk.dim(date)}`);
|
|
5509
|
+
for (const [selector, value] of seenFills) {
|
|
5510
|
+
steps.push(`Fill ${selector} with "${value}"`);
|
|
5250
5511
|
}
|
|
5251
|
-
lines.push("");
|
|
5252
|
-
return lines.join(`
|
|
5253
|
-
`);
|
|
5254
|
-
}
|
|
5255
|
-
function getScenarioRunStats(scenarioId) {
|
|
5256
|
-
const db2 = getDatabase();
|
|
5257
|
-
const lastRow = db2.query("SELECT status FROM results WHERE scenario_id = ? ORDER BY created_at DESC LIMIT 1").get(scenarioId);
|
|
5258
|
-
const statsRow = db2.query("SELECT COUNT(*) as total, SUM(CASE WHEN status = 'passed' THEN 1 ELSE 0 END) as passed FROM results WHERE scenario_id = ?").get(scenarioId);
|
|
5259
5512
|
return {
|
|
5260
|
-
|
|
5261
|
-
|
|
5513
|
+
name,
|
|
5514
|
+
description: `Recorded session on ${recording.url} (${(recording.duration / 1000).toFixed(0)}s, ${recording.actions.length} actions)`,
|
|
5515
|
+
steps,
|
|
5516
|
+
tags: ["recorded"],
|
|
5517
|
+
projectId
|
|
5262
5518
|
};
|
|
5263
5519
|
}
|
|
5264
|
-
function
|
|
5265
|
-
const
|
|
5266
|
-
|
|
5267
|
-
|
|
5268
|
-
|
|
5269
|
-
if (scenarios.length === 0) {
|
|
5270
|
-
lines.push(chalk.dim(" No scenarios found. Use 'testers add' to create one."));
|
|
5271
|
-
lines.push("");
|
|
5272
|
-
return lines.join(`
|
|
5273
|
-
`);
|
|
5274
|
-
}
|
|
5275
|
-
for (const s of scenarios) {
|
|
5276
|
-
const priorityColor = s.priority === "critical" ? chalk.red : s.priority === "high" ? chalk.yellow : s.priority === "medium" ? chalk.blue : chalk.dim;
|
|
5277
|
-
const tags = s.tags.length > 0 ? chalk.dim(` [${s.tags.join(", ")}]`) : "";
|
|
5278
|
-
let lastStatusIcon = chalk.dim("\u2014");
|
|
5279
|
-
let passRateStr = chalk.dim("\u2014");
|
|
5280
|
-
if (s.id) {
|
|
5281
|
-
const stats = getScenarioRunStats(s.id);
|
|
5282
|
-
if (stats.lastStatus === "passed")
|
|
5283
|
-
lastStatusIcon = chalk.green("\u2713");
|
|
5284
|
-
else if (stats.lastStatus === "failed")
|
|
5285
|
-
lastStatusIcon = chalk.red("\u2717");
|
|
5286
|
-
else if (stats.lastStatus === "error")
|
|
5287
|
-
lastStatusIcon = chalk.yellow("!");
|
|
5288
|
-
else if (stats.lastStatus === "skipped")
|
|
5289
|
-
lastStatusIcon = chalk.dim("~");
|
|
5290
|
-
passRateStr = stats.passRate === "\u2014" ? chalk.dim("\u2014") : chalk.dim(stats.passRate);
|
|
5291
|
-
}
|
|
5292
|
-
lines.push(` ${chalk.cyan(s.shortId)} ${s.name} ${priorityColor(s.priority)}${tags} ${lastStatusIcon} ${passRateStr}`);
|
|
5293
|
-
}
|
|
5294
|
-
lines.push("");
|
|
5295
|
-
return lines.join(`
|
|
5296
|
-
`);
|
|
5520
|
+
async function recordAndSave(url, name, projectId) {
|
|
5521
|
+
const recording = await recordSession(url);
|
|
5522
|
+
const input = actionsToScenarioInput(recording, name, projectId);
|
|
5523
|
+
const scenario = createScenario(input);
|
|
5524
|
+
return { recording, scenario };
|
|
5297
5525
|
}
|
|
5526
|
+
var init_recorder = __esm(() => {
|
|
5527
|
+
init_scenarios();
|
|
5528
|
+
});
|
|
5298
5529
|
|
|
5299
|
-
// src/lib/
|
|
5300
|
-
|
|
5301
|
-
|
|
5302
|
-
|
|
5303
|
-
|
|
5304
|
-
|
|
5305
|
-
|
|
5306
|
-
|
|
5307
|
-
|
|
5308
|
-
|
|
5309
|
-
|
|
5310
|
-
|
|
5311
|
-
|
|
5312
|
-
|
|
5313
|
-
|
|
5314
|
-
|
|
5315
|
-
|
|
5316
|
-
|
|
5317
|
-
const
|
|
5318
|
-
|
|
5319
|
-
|
|
5320
|
-
}
|
|
5321
|
-
|
|
5322
|
-
|
|
5323
|
-
|
|
5324
|
-
|
|
5325
|
-
|
|
5326
|
-
if (options.status) {
|
|
5327
|
-
query += " AND status = ?";
|
|
5328
|
-
params.push(options.status);
|
|
5329
|
-
} else {
|
|
5330
|
-
query += " AND status IN ('pending', 'in_progress')";
|
|
5530
|
+
// src/lib/affected.ts
|
|
5531
|
+
var exports_affected = {};
|
|
5532
|
+
__export(exports_affected, {
|
|
5533
|
+
matchFilesToScenarios: () => matchFilesToScenarios
|
|
5534
|
+
});
|
|
5535
|
+
function globToRegex(glob) {
|
|
5536
|
+
const escaped = glob.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*\*/g, "\x00DS\x00").replace(/\*/g, "[^/]*").replace(/\x00DS\x00/g, ".*");
|
|
5537
|
+
return new RegExp(`^${escaped}$`, "i");
|
|
5538
|
+
}
|
|
5539
|
+
function matchFilesToScenarios(filePaths, scenarios, mappings = []) {
|
|
5540
|
+
if (filePaths.length === 0)
|
|
5541
|
+
return scenarios;
|
|
5542
|
+
const compiledMappings = mappings.map((m) => ({
|
|
5543
|
+
regex: globToRegex(m.glob),
|
|
5544
|
+
tags: m.tags
|
|
5545
|
+
}));
|
|
5546
|
+
const normPaths = filePaths.map((p) => p.replace(/\\/g, "/").toLowerCase());
|
|
5547
|
+
const matchedIds = new Set;
|
|
5548
|
+
for (const scenario of scenarios) {
|
|
5549
|
+
let matched = false;
|
|
5550
|
+
if (!matched) {
|
|
5551
|
+
for (const { regex, tags } of compiledMappings) {
|
|
5552
|
+
if (normPaths.some((fp) => regex.test(fp)) && tags.some((tag) => scenario.tags.includes(tag))) {
|
|
5553
|
+
matched = true;
|
|
5554
|
+
break;
|
|
5555
|
+
}
|
|
5556
|
+
}
|
|
5331
5557
|
}
|
|
5332
|
-
if (
|
|
5333
|
-
|
|
5334
|
-
|
|
5558
|
+
if (!matched && scenario.targetPath) {
|
|
5559
|
+
const segments = scenario.targetPath.replace(/^\//, "").split("/").filter((s) => s.length > 2);
|
|
5560
|
+
if (segments.some((seg) => normPaths.some((fp) => fp.includes(seg.toLowerCase())))) {
|
|
5561
|
+
matched = true;
|
|
5562
|
+
}
|
|
5335
5563
|
}
|
|
5336
|
-
if (
|
|
5337
|
-
|
|
5338
|
-
|
|
5339
|
-
|
|
5340
|
-
|
|
5564
|
+
if (!matched) {
|
|
5565
|
+
for (const tag of scenario.tags) {
|
|
5566
|
+
if (tag.length > 2 && normPaths.some((fp) => fp.includes(tag.toLowerCase()))) {
|
|
5567
|
+
matched = true;
|
|
5568
|
+
break;
|
|
5569
|
+
}
|
|
5341
5570
|
}
|
|
5342
5571
|
}
|
|
5343
|
-
|
|
5344
|
-
|
|
5345
|
-
|
|
5346
|
-
|
|
5347
|
-
|
|
5348
|
-
return options.tags.some((tag) => taskTags.includes(tag));
|
|
5349
|
-
});
|
|
5572
|
+
if (!matched) {
|
|
5573
|
+
const nameWords = scenario.name.toLowerCase().split(/[\s\-_/]+/).filter((w) => w.length > 3);
|
|
5574
|
+
if (nameWords.some((word) => normPaths.some((fp) => fp.includes(word)))) {
|
|
5575
|
+
matched = true;
|
|
5576
|
+
}
|
|
5350
5577
|
}
|
|
5351
|
-
|
|
5352
|
-
|
|
5353
|
-
db2.close();
|
|
5578
|
+
if (matched)
|
|
5579
|
+
matchedIds.add(scenario.id);
|
|
5354
5580
|
}
|
|
5581
|
+
return scenarios.filter((s) => matchedIds.has(s.id));
|
|
5355
5582
|
}
|
|
5356
|
-
|
|
5357
|
-
|
|
5358
|
-
|
|
5359
|
-
|
|
5360
|
-
|
|
5361
|
-
|
|
5362
|
-
|
|
5363
|
-
|
|
5364
|
-
|
|
5365
|
-
|
|
5366
|
-
|
|
5583
|
+
|
|
5584
|
+
// src/lib/git-watch.ts
|
|
5585
|
+
var exports_git_watch = {};
|
|
5586
|
+
__export(exports_git_watch, {
|
|
5587
|
+
startGitWatcher: () => startGitWatcher
|
|
5588
|
+
});
|
|
5589
|
+
import { execSync as execSync2 } from "child_process";
|
|
5590
|
+
import chalk5 from "chalk";
|
|
5591
|
+
function getLatestCommitHash(dir) {
|
|
5592
|
+
try {
|
|
5593
|
+
return execSync2("git rev-parse HEAD", { cwd: dir, stdio: ["pipe", "pipe", "pipe"] }).toString().trim();
|
|
5594
|
+
} catch {
|
|
5595
|
+
return null;
|
|
5596
|
+
}
|
|
5597
|
+
}
|
|
5598
|
+
function getChangedFiles(fromHash, toHash, dir) {
|
|
5599
|
+
try {
|
|
5600
|
+
const out = execSync2(`git diff --name-only ${fromHash} ${toHash}`, {
|
|
5601
|
+
cwd: dir,
|
|
5602
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
5603
|
+
}).toString();
|
|
5604
|
+
return out.split(`
|
|
5605
|
+
`).filter(Boolean);
|
|
5606
|
+
} catch {
|
|
5607
|
+
return [];
|
|
5608
|
+
}
|
|
5609
|
+
}
|
|
5610
|
+
async function startGitWatcher(options) {
|
|
5611
|
+
const {
|
|
5612
|
+
url,
|
|
5613
|
+
dir = process.cwd(),
|
|
5614
|
+
pollIntervalMs = 1e4,
|
|
5615
|
+
mappings = [],
|
|
5616
|
+
projectId,
|
|
5617
|
+
tags,
|
|
5618
|
+
...runOpts
|
|
5619
|
+
} = options;
|
|
5620
|
+
let lastHash = getLatestCommitHash(dir);
|
|
5621
|
+
if (!lastHash) {
|
|
5622
|
+
console.error(chalk5.red(" [git-watch] Not a git repository or git not available."));
|
|
5623
|
+
process.exit(1);
|
|
5624
|
+
}
|
|
5625
|
+
console.log("");
|
|
5626
|
+
console.log(chalk5.bold(" Testers Git Watch"));
|
|
5627
|
+
console.log(chalk5.dim(` Directory: ${dir}`));
|
|
5628
|
+
console.log(chalk5.dim(` Target URL: ${url}`));
|
|
5629
|
+
console.log(chalk5.dim(` Poll every: ${pollIntervalMs / 1000}s`));
|
|
5630
|
+
console.log(chalk5.dim(` Last commit: ${lastHash.slice(0, 8)}`));
|
|
5631
|
+
console.log("");
|
|
5632
|
+
console.log(chalk5.dim(" Watching for new commits... (Ctrl+C to stop)"));
|
|
5633
|
+
console.log("");
|
|
5634
|
+
const cleanup = () => {
|
|
5635
|
+
console.log("");
|
|
5636
|
+
console.log(chalk5.dim(" Git watch stopped."));
|
|
5637
|
+
process.exit(0);
|
|
5638
|
+
};
|
|
5639
|
+
process.on("SIGINT", cleanup);
|
|
5640
|
+
process.on("SIGTERM", cleanup);
|
|
5641
|
+
while (true) {
|
|
5642
|
+
await new Promise((r) => setTimeout(r, pollIntervalMs));
|
|
5643
|
+
const currentHash = getLatestCommitHash(dir);
|
|
5644
|
+
if (!currentHash || currentHash === lastHash)
|
|
5645
|
+
continue;
|
|
5646
|
+
const changedFiles = getChangedFiles(lastHash, currentHash, dir);
|
|
5647
|
+
lastHash = currentHash;
|
|
5648
|
+
console.log(chalk5.yellow(` [commit] ${currentHash.slice(0, 8)} \u2014 ${changedFiles.length} file(s) changed`));
|
|
5649
|
+
for (const f of changedFiles.slice(0, 10)) {
|
|
5650
|
+
console.log(chalk5.dim(` ${f}`));
|
|
5651
|
+
}
|
|
5652
|
+
if (changedFiles.length > 10) {
|
|
5653
|
+
console.log(chalk5.dim(` \u2026 and ${changedFiles.length - 10} more`));
|
|
5654
|
+
}
|
|
5655
|
+
const allScenarios = listScenarios({ projectId, tags });
|
|
5656
|
+
const matched = matchFilesToScenarios(changedFiles, allScenarios, mappings);
|
|
5657
|
+
if (matched.length === 0) {
|
|
5658
|
+
console.log(chalk5.dim(" No matching scenarios \u2014 skipping run."));
|
|
5659
|
+
console.log("");
|
|
5660
|
+
continue;
|
|
5661
|
+
}
|
|
5662
|
+
console.log(chalk5.blue(` [running] ${matched.length} affected scenario(s) against ${url}...`));
|
|
5663
|
+
console.log("");
|
|
5664
|
+
try {
|
|
5665
|
+
const { run, results } = await runBatch(matched, { url, projectId, ...runOpts });
|
|
5666
|
+
console.log(formatTerminal(run, results));
|
|
5667
|
+
if (run.status === "failed") {
|
|
5668
|
+
const failedResults = results.filter((r) => r.status === "failed" || r.status === "error");
|
|
5669
|
+
notifyFailureToConversations(run, failedResults, matched).catch(() => {});
|
|
5367
5670
|
}
|
|
5671
|
+
} catch (error) {
|
|
5672
|
+
console.error(chalk5.red(` Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
5368
5673
|
}
|
|
5674
|
+
console.log(chalk5.dim(" Watching for new commits..."));
|
|
5675
|
+
console.log("");
|
|
5369
5676
|
}
|
|
5370
|
-
return {
|
|
5371
|
-
name: task.title.replace(/^(OPE\d+-\d+|[A-Z]+-\d+):\s*/, ""),
|
|
5372
|
-
description: task.description || task.title,
|
|
5373
|
-
steps,
|
|
5374
|
-
tags,
|
|
5375
|
-
priority,
|
|
5376
|
-
projectId,
|
|
5377
|
-
metadata: { todosTaskId: task.id, todosShortId: task.short_id }
|
|
5378
|
-
};
|
|
5379
5677
|
}
|
|
5380
|
-
|
|
5381
|
-
|
|
5382
|
-
|
|
5383
|
-
|
|
5384
|
-
|
|
5385
|
-
|
|
5386
|
-
|
|
5387
|
-
|
|
5388
|
-
|
|
5389
|
-
|
|
5390
|
-
|
|
5391
|
-
|
|
5392
|
-
|
|
5393
|
-
|
|
5394
|
-
|
|
5395
|
-
|
|
5396
|
-
|
|
5397
|
-
|
|
5678
|
+
var init_git_watch = __esm(() => {
|
|
5679
|
+
init_scenarios();
|
|
5680
|
+
init_runner();
|
|
5681
|
+
init_reporter();
|
|
5682
|
+
init_failure_pipeline();
|
|
5683
|
+
});
|
|
5684
|
+
|
|
5685
|
+
// src/db/agents.ts
|
|
5686
|
+
var exports_agents = {};
|
|
5687
|
+
__export(exports_agents, {
|
|
5688
|
+
setAgentFocus: () => setAgentFocus,
|
|
5689
|
+
registerAgent: () => registerAgent,
|
|
5690
|
+
listAgents: () => listAgents,
|
|
5691
|
+
heartbeatAgent: () => heartbeatAgent,
|
|
5692
|
+
getAgentByName: () => getAgentByName,
|
|
5693
|
+
getAgent: () => getAgent
|
|
5694
|
+
});
|
|
5695
|
+
function registerAgent(input) {
|
|
5696
|
+
const db2 = getDatabase();
|
|
5697
|
+
const existing = db2.query("SELECT * FROM agents WHERE name = ?").get(input.name);
|
|
5698
|
+
if (existing) {
|
|
5699
|
+
db2.query("UPDATE agents SET last_seen_at = ? WHERE id = ?").run(now(), existing.id);
|
|
5700
|
+
return getAgent(existing.id);
|
|
5398
5701
|
}
|
|
5399
|
-
|
|
5702
|
+
const id = uuid();
|
|
5703
|
+
const timestamp = now();
|
|
5704
|
+
db2.query(`
|
|
5705
|
+
INSERT INTO agents (id, name, description, role, metadata, created_at, last_seen_at)
|
|
5706
|
+
VALUES (?, ?, ?, ?, '{}', ?, ?)
|
|
5707
|
+
`).run(id, input.name, input.description ?? null, input.role ?? null, timestamp, timestamp);
|
|
5708
|
+
return getAgent(id);
|
|
5709
|
+
}
|
|
5710
|
+
function getAgent(id) {
|
|
5711
|
+
const db2 = getDatabase();
|
|
5712
|
+
const row = db2.query("SELECT * FROM agents WHERE id = ?").get(id);
|
|
5713
|
+
return row ? agentFromRow(row) : null;
|
|
5714
|
+
}
|
|
5715
|
+
function getAgentByName(name) {
|
|
5716
|
+
const db2 = getDatabase();
|
|
5717
|
+
const row = db2.query("SELECT * FROM agents WHERE name = ?").get(name);
|
|
5718
|
+
return row ? agentFromRow(row) : null;
|
|
5719
|
+
}
|
|
5720
|
+
function listAgents() {
|
|
5721
|
+
const db2 = getDatabase();
|
|
5722
|
+
const rows = db2.query("SELECT * FROM agents ORDER BY created_at DESC").all();
|
|
5723
|
+
return rows.map(agentFromRow);
|
|
5724
|
+
}
|
|
5725
|
+
function heartbeatAgent(id) {
|
|
5726
|
+
const db2 = getDatabase();
|
|
5727
|
+
const affected = db2.query("UPDATE agents SET last_seen_at = ? WHERE id = ?").run(now(), id);
|
|
5728
|
+
if (affected.changes === 0)
|
|
5729
|
+
return null;
|
|
5730
|
+
return getAgent(id);
|
|
5731
|
+
}
|
|
5732
|
+
function setAgentFocus(id, scenarioId) {
|
|
5733
|
+
const db2 = getDatabase();
|
|
5734
|
+
const agent = getAgent(id);
|
|
5735
|
+
if (!agent)
|
|
5736
|
+
return null;
|
|
5737
|
+
const metadata = { ...agent.metadata ?? {}, focus: scenarioId };
|
|
5738
|
+
db2.query("UPDATE agents SET metadata = ?, last_seen_at = ? WHERE id = ?").run(JSON.stringify(metadata), now(), id);
|
|
5739
|
+
return getAgent(id);
|
|
5400
5740
|
}
|
|
5741
|
+
var init_agents = __esm(() => {
|
|
5742
|
+
init_types();
|
|
5743
|
+
init_database();
|
|
5744
|
+
});
|
|
5745
|
+
|
|
5746
|
+
// node_modules/commander/esm.mjs
|
|
5747
|
+
var import__ = __toESM(require_commander(), 1);
|
|
5748
|
+
var {
|
|
5749
|
+
program,
|
|
5750
|
+
createCommand,
|
|
5751
|
+
createArgument,
|
|
5752
|
+
createOption,
|
|
5753
|
+
CommanderError,
|
|
5754
|
+
InvalidArgumentError,
|
|
5755
|
+
InvalidOptionArgumentError,
|
|
5756
|
+
Command,
|
|
5757
|
+
Argument,
|
|
5758
|
+
Option,
|
|
5759
|
+
Help
|
|
5760
|
+
} = import__.default;
|
|
5761
|
+
|
|
5762
|
+
// src/cli/index.tsx
|
|
5763
|
+
import chalk6 from "chalk";
|
|
5764
|
+
// package.json
|
|
5765
|
+
var package_default = {
|
|
5766
|
+
name: "@hasna/testers",
|
|
5767
|
+
version: "0.0.13",
|
|
5768
|
+
description: "AI-powered QA testing CLI \u2014 spawns cheap AI agents to test web apps with headless browsers",
|
|
5769
|
+
type: "module",
|
|
5770
|
+
main: "dist/index.js",
|
|
5771
|
+
types: "dist/index.d.ts",
|
|
5772
|
+
bin: {
|
|
5773
|
+
testers: "dist/cli/index.js",
|
|
5774
|
+
"testers-mcp": "dist/mcp/index.js",
|
|
5775
|
+
"testers-serve": "dist/server/index.js"
|
|
5776
|
+
},
|
|
5777
|
+
exports: {
|
|
5778
|
+
".": {
|
|
5779
|
+
types: "./dist/index.d.ts",
|
|
5780
|
+
import: "./dist/index.js"
|
|
5781
|
+
}
|
|
5782
|
+
},
|
|
5783
|
+
files: [
|
|
5784
|
+
"dist/",
|
|
5785
|
+
"dashboard/dist/",
|
|
5786
|
+
"LICENSE",
|
|
5787
|
+
"README.md"
|
|
5788
|
+
],
|
|
5789
|
+
scripts: {
|
|
5790
|
+
build: "bun run build:dashboard && bun run build:cli && bun run build:mcp && bun run build:server && bun run build:lib && bun run build:types",
|
|
5791
|
+
"build:cli": "bun build src/cli/index.tsx --outdir dist/cli --target bun --external ink --external react --external chalk --external @modelcontextprotocol/sdk --external @anthropic-ai/sdk --external playwright",
|
|
5792
|
+
"build:mcp": "bun build src/mcp/index.ts --outdir dist/mcp --target bun --external @modelcontextprotocol/sdk --external @anthropic-ai/sdk --external playwright",
|
|
5793
|
+
"build:server": "bun build src/server/index.ts --outdir dist/server --target bun --external @anthropic-ai/sdk --external playwright",
|
|
5794
|
+
"build:lib": "bun build src/index.ts --outdir dist --target bun --external playwright --external @anthropic-ai/sdk --external @modelcontextprotocol/sdk",
|
|
5795
|
+
"build:types": "NODE_OPTIONS='--max-old-space-size=8192' tsc --emitDeclarationOnly --outDir dist --skipLibCheck",
|
|
5796
|
+
"build:dashboard": "cd dashboard && bun run build",
|
|
5797
|
+
typecheck: "tsc --noEmit",
|
|
5798
|
+
test: "bun test",
|
|
5799
|
+
"dev:cli": "bun run src/cli/index.tsx",
|
|
5800
|
+
"dev:mcp": "bun run src/mcp/index.ts",
|
|
5801
|
+
"dev:serve": "bun run src/server/index.ts",
|
|
5802
|
+
prepublishOnly: "bun run build"
|
|
5803
|
+
},
|
|
5804
|
+
dependencies: {
|
|
5805
|
+
"@anthropic-ai/sdk": "^0.52.0",
|
|
5806
|
+
"@modelcontextprotocol/sdk": "^1.12.1",
|
|
5807
|
+
chalk: "^5.4.1",
|
|
5808
|
+
commander: "^13.1.0",
|
|
5809
|
+
ink: "^5.2.0",
|
|
5810
|
+
playwright: "^1.50.0",
|
|
5811
|
+
react: "^18.3.1",
|
|
5812
|
+
zod: "^3.24.2"
|
|
5813
|
+
},
|
|
5814
|
+
devDependencies: {
|
|
5815
|
+
"@types/bun": "latest",
|
|
5816
|
+
"@types/react": "^18.3.18",
|
|
5817
|
+
typescript: "^5.7.3"
|
|
5818
|
+
},
|
|
5819
|
+
engines: {
|
|
5820
|
+
bun: ">=1.0.0"
|
|
5821
|
+
},
|
|
5822
|
+
publishConfig: {
|
|
5823
|
+
access: "public",
|
|
5824
|
+
registry: "https://registry.npmjs.org/"
|
|
5825
|
+
},
|
|
5826
|
+
repository: {
|
|
5827
|
+
type: "git",
|
|
5828
|
+
url: "https://github.com/hasna/open-testers.git"
|
|
5829
|
+
},
|
|
5830
|
+
license: "MIT",
|
|
5831
|
+
keywords: [
|
|
5832
|
+
"testing",
|
|
5833
|
+
"qa",
|
|
5834
|
+
"ai",
|
|
5835
|
+
"playwright",
|
|
5836
|
+
"browser",
|
|
5837
|
+
"screenshot",
|
|
5838
|
+
"automation",
|
|
5839
|
+
"cli",
|
|
5840
|
+
"mcp"
|
|
5841
|
+
]
|
|
5842
|
+
};
|
|
5843
|
+
|
|
5844
|
+
// src/cli/index.tsx
|
|
5845
|
+
init_scenarios();
|
|
5846
|
+
init_runs();
|
|
5847
|
+
init_results();
|
|
5848
|
+
init_screenshots();
|
|
5849
|
+
init_runner();
|
|
5850
|
+
init_reporter();
|
|
5851
|
+
init_config();
|
|
5852
|
+
init_todos_connector();
|
|
5853
|
+
init_browser();
|
|
5854
|
+
import { render, Box, Text, useInput, useApp } from "ink";
|
|
5855
|
+
import React, { useState } from "react";
|
|
5856
|
+
import { readFileSync as readFileSync6, readdirSync, writeFileSync as writeFileSync3 } from "fs";
|
|
5857
|
+
import { createInterface } from "readline";
|
|
5858
|
+
import { join as join6, resolve } from "path";
|
|
5401
5859
|
|
|
5402
5860
|
// src/lib/init.ts
|
|
5403
5861
|
init_scenarios();
|
|
@@ -5679,7 +6137,10 @@ function initProject(options) {
|
|
|
5679
6137
|
|
|
5680
6138
|
// src/lib/smoke.ts
|
|
5681
6139
|
init_scenarios();
|
|
6140
|
+
init_runner();
|
|
5682
6141
|
init_runs();
|
|
6142
|
+
init_config();
|
|
6143
|
+
init_ai_client();
|
|
5683
6144
|
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:
|
|
5684
6145
|
|
|
5685
6146
|
1. Start at the given URL and take a screenshot
|
|
@@ -5899,8 +6360,9 @@ function formatSmokeReport(result) {
|
|
|
5899
6360
|
|
|
5900
6361
|
// src/lib/diff.ts
|
|
5901
6362
|
init_runs();
|
|
5902
|
-
|
|
6363
|
+
init_results();
|
|
5903
6364
|
init_scenarios();
|
|
6365
|
+
import chalk2 from "chalk";
|
|
5904
6366
|
function diffRuns(runId1, runId2) {
|
|
5905
6367
|
const run1 = getRun(runId1);
|
|
5906
6368
|
if (!run1) {
|
|
@@ -6047,11 +6509,13 @@ function formatDiffJSON(diff) {
|
|
|
6047
6509
|
}
|
|
6048
6510
|
|
|
6049
6511
|
// src/lib/visual-diff.ts
|
|
6050
|
-
|
|
6051
|
-
|
|
6512
|
+
init_screenshots();
|
|
6513
|
+
init_results();
|
|
6052
6514
|
init_runs();
|
|
6053
6515
|
init_scenarios();
|
|
6054
6516
|
init_database();
|
|
6517
|
+
import { readFileSync as readFileSync3, existsSync as existsSync6 } from "fs";
|
|
6518
|
+
import chalk3 from "chalk";
|
|
6055
6519
|
var DEFAULT_THRESHOLD = 0.1;
|
|
6056
6520
|
function setBaseline(runId) {
|
|
6057
6521
|
const run = getRun(runId);
|
|
@@ -6178,8 +6642,10 @@ function formatVisualDiffTerminal(results, threshold = DEFAULT_THRESHOLD) {
|
|
|
6178
6642
|
|
|
6179
6643
|
// src/lib/report.ts
|
|
6180
6644
|
init_runs();
|
|
6181
|
-
|
|
6645
|
+
init_results();
|
|
6646
|
+
init_screenshots();
|
|
6182
6647
|
init_scenarios();
|
|
6648
|
+
import { readFileSync as readFileSync4, existsSync as existsSync7 } from "fs";
|
|
6183
6649
|
function imageToBase64(filePath) {
|
|
6184
6650
|
if (!filePath || !existsSync7(filePath))
|
|
6185
6651
|
return "";
|
|
@@ -6378,6 +6844,7 @@ function generateLatestReport() {
|
|
|
6378
6844
|
|
|
6379
6845
|
// src/lib/costs.ts
|
|
6380
6846
|
init_database();
|
|
6847
|
+
init_config();
|
|
6381
6848
|
import chalk4 from "chalk";
|
|
6382
6849
|
function getDateFilter(period) {
|
|
6383
6850
|
switch (period) {
|
|
@@ -7177,10 +7644,10 @@ async function runInteractiveAdd(projectId) {
|
|
|
7177
7644
|
priority: result.priority,
|
|
7178
7645
|
projectId
|
|
7179
7646
|
});
|
|
7180
|
-
log(
|
|
7181
|
-
Created scenario ${
|
|
7647
|
+
log(chalk6.green(`
|
|
7648
|
+
Created scenario ${chalk6.bold(scenario.shortId)}: ${scenario.name}`));
|
|
7182
7649
|
} else {
|
|
7183
|
-
log(
|
|
7650
|
+
log(chalk6.dim(`
|
|
7184
7651
|
Cancelled.`));
|
|
7185
7652
|
}
|
|
7186
7653
|
}
|
|
@@ -7239,19 +7706,19 @@ program2.command("add [name]").alias("create").description("Create a new test sc
|
|
|
7239
7706
|
return;
|
|
7240
7707
|
}
|
|
7241
7708
|
if (!name) {
|
|
7242
|
-
logError(
|
|
7709
|
+
logError(chalk6.red("Error: scenario name is required"));
|
|
7243
7710
|
process.exit(1);
|
|
7244
7711
|
}
|
|
7245
7712
|
if (opts.template) {
|
|
7246
7713
|
const template = getTemplate(opts.template);
|
|
7247
7714
|
if (!template) {
|
|
7248
|
-
logError(
|
|
7715
|
+
logError(chalk6.red(`Unknown template: ${opts.template}. Available: ${listTemplateNames().join(", ")}`));
|
|
7249
7716
|
process.exit(1);
|
|
7250
7717
|
}
|
|
7251
7718
|
const projectId2 = resolveProject(opts.project);
|
|
7252
7719
|
for (const input of template) {
|
|
7253
7720
|
const s = createScenario({ ...input, projectId: projectId2 });
|
|
7254
|
-
log(
|
|
7721
|
+
log(chalk6.green(` Created ${s.shortId}: ${s.name}`));
|
|
7255
7722
|
}
|
|
7256
7723
|
return;
|
|
7257
7724
|
}
|
|
@@ -7270,9 +7737,9 @@ program2.command("add [name]").alias("create").description("Create a new test sc
|
|
|
7270
7737
|
assertions: assertions.length > 0 ? assertions : undefined,
|
|
7271
7738
|
projectId
|
|
7272
7739
|
});
|
|
7273
|
-
log(
|
|
7740
|
+
log(chalk6.green(`Created scenario ${chalk6.bold(scenario.shortId)}: ${scenario.name}`));
|
|
7274
7741
|
} catch (error) {
|
|
7275
|
-
logError(
|
|
7742
|
+
logError(chalk6.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
7276
7743
|
process.exit(1);
|
|
7277
7744
|
}
|
|
7278
7745
|
});
|
|
@@ -7294,7 +7761,7 @@ program2.command("list").alias("ls").description("List test scenarios").option("
|
|
|
7294
7761
|
log(formatScenarioList(scenarios));
|
|
7295
7762
|
}
|
|
7296
7763
|
} catch (error) {
|
|
7297
|
-
logError(
|
|
7764
|
+
logError(chalk6.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
7298
7765
|
process.exit(1);
|
|
7299
7766
|
}
|
|
7300
7767
|
});
|
|
@@ -7302,7 +7769,7 @@ program2.command("show <id>").description("Show scenario details").option("--jso
|
|
|
7302
7769
|
try {
|
|
7303
7770
|
const scenario = getScenario(id) ?? getScenarioByShortId(id);
|
|
7304
7771
|
if (!scenario) {
|
|
7305
|
-
logError(
|
|
7772
|
+
logError(chalk6.red(`Scenario not found: ${id}`));
|
|
7306
7773
|
process.exit(1);
|
|
7307
7774
|
}
|
|
7308
7775
|
if (opts.json) {
|
|
@@ -7310,29 +7777,29 @@ program2.command("show <id>").description("Show scenario details").option("--jso
|
|
|
7310
7777
|
return;
|
|
7311
7778
|
}
|
|
7312
7779
|
log("");
|
|
7313
|
-
log(
|
|
7780
|
+
log(chalk6.bold(` Scenario ${scenario.shortId}`));
|
|
7314
7781
|
log(` Name: ${scenario.name}`);
|
|
7315
|
-
log(` ID: ${
|
|
7782
|
+
log(` ID: ${chalk6.dim(scenario.id)}`);
|
|
7316
7783
|
log(` Description: ${scenario.description}`);
|
|
7317
7784
|
log(` Priority: ${scenario.priority}`);
|
|
7318
|
-
log(` Model: ${scenario.model ??
|
|
7319
|
-
log(` Tags: ${scenario.tags.length > 0 ? scenario.tags.join(", ") :
|
|
7320
|
-
log(` Path: ${scenario.targetPath ??
|
|
7785
|
+
log(` Model: ${scenario.model ?? chalk6.dim("default")}`);
|
|
7786
|
+
log(` Tags: ${scenario.tags.length > 0 ? scenario.tags.join(", ") : chalk6.dim("none")}`);
|
|
7787
|
+
log(` Path: ${scenario.targetPath ?? chalk6.dim("none")}`);
|
|
7321
7788
|
log(` Auth: ${scenario.requiresAuth ? "yes" : "no"}`);
|
|
7322
|
-
log(` Timeout: ${scenario.timeoutMs ? `${scenario.timeoutMs}ms` :
|
|
7789
|
+
log(` Timeout: ${scenario.timeoutMs ? `${scenario.timeoutMs}ms` : chalk6.dim("default")}`);
|
|
7323
7790
|
log(` Version: ${scenario.version}`);
|
|
7324
7791
|
log(` Created: ${scenario.createdAt}`);
|
|
7325
7792
|
log(` Updated: ${scenario.updatedAt}`);
|
|
7326
7793
|
if (scenario.steps.length > 0) {
|
|
7327
7794
|
log("");
|
|
7328
|
-
log(
|
|
7795
|
+
log(chalk6.bold(" Steps:"));
|
|
7329
7796
|
for (let i = 0;i < scenario.steps.length; i++) {
|
|
7330
7797
|
log(` ${i + 1}. ${scenario.steps[i]}`);
|
|
7331
7798
|
}
|
|
7332
7799
|
}
|
|
7333
7800
|
log("");
|
|
7334
7801
|
} catch (error) {
|
|
7335
|
-
logError(
|
|
7802
|
+
logError(chalk6.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
7336
7803
|
process.exit(1);
|
|
7337
7804
|
}
|
|
7338
7805
|
});
|
|
@@ -7352,7 +7819,7 @@ program2.command("update <id>").description("Update a scenario").option("-n, --n
|
|
|
7352
7819
|
try {
|
|
7353
7820
|
const scenario = getScenario(id) ?? getScenarioByShortId(id);
|
|
7354
7821
|
if (!scenario) {
|
|
7355
|
-
logError(
|
|
7822
|
+
logError(chalk6.red(`Scenario not found: ${id}`));
|
|
7356
7823
|
process.exit(1);
|
|
7357
7824
|
}
|
|
7358
7825
|
let newTags;
|
|
@@ -7374,12 +7841,12 @@ program2.command("update <id>").description("Update a scenario").option("-n, --n
|
|
|
7374
7841
|
priority: opts.priority,
|
|
7375
7842
|
model: opts.model
|
|
7376
7843
|
}, scenario.version);
|
|
7377
|
-
log(
|
|
7844
|
+
log(chalk6.green(`Updated scenario ${chalk6.bold(updated.shortId)}: ${updated.name}`));
|
|
7378
7845
|
if (newTags !== undefined) {
|
|
7379
|
-
log(
|
|
7846
|
+
log(chalk6.dim(` Tags: [${updated.tags.join(", ")}]`));
|
|
7380
7847
|
}
|
|
7381
7848
|
} catch (error) {
|
|
7382
|
-
logError(
|
|
7849
|
+
logError(chalk6.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
7383
7850
|
process.exit(1);
|
|
7384
7851
|
}
|
|
7385
7852
|
});
|
|
@@ -7387,11 +7854,50 @@ program2.command("delete <id>").description("Delete a scenario").option("-y, --y
|
|
|
7387
7854
|
try {
|
|
7388
7855
|
const scenario = getScenario(id) ?? getScenarioByShortId(id);
|
|
7389
7856
|
if (!scenario) {
|
|
7390
|
-
logError(
|
|
7857
|
+
logError(chalk6.red(`Scenario not found: ${id}`));
|
|
7858
|
+
process.exit(1);
|
|
7859
|
+
}
|
|
7860
|
+
if (!opts.yes) {
|
|
7861
|
+
process.stdout.write(chalk6.yellow(`Delete scenario ${scenario.shortId} "${scenario.name}"? [y/N] `));
|
|
7862
|
+
const answer = await new Promise((resolve2) => {
|
|
7863
|
+
let buf = "";
|
|
7864
|
+
process.stdin.setRawMode?.(true);
|
|
7865
|
+
process.stdin.resume();
|
|
7866
|
+
process.stdin.once("data", (chunk) => {
|
|
7867
|
+
buf = chunk.toString().trim().toLowerCase();
|
|
7868
|
+
process.stdin.setRawMode?.(false);
|
|
7869
|
+
process.stdin.pause();
|
|
7870
|
+
process.stdout.write(`
|
|
7871
|
+
`);
|
|
7872
|
+
resolve2(buf);
|
|
7873
|
+
});
|
|
7874
|
+
});
|
|
7875
|
+
if (answer !== "y" && answer !== "yes") {
|
|
7876
|
+
log(chalk6.dim("Cancelled."));
|
|
7877
|
+
return;
|
|
7878
|
+
}
|
|
7879
|
+
}
|
|
7880
|
+
const deleted = deleteScenario(scenario.id);
|
|
7881
|
+
if (deleted) {
|
|
7882
|
+
log(chalk6.green(`Deleted scenario ${scenario.shortId}: ${scenario.name}`));
|
|
7883
|
+
} else {
|
|
7884
|
+
logError(chalk6.red(`Failed to delete scenario: ${id}`));
|
|
7885
|
+
process.exit(1);
|
|
7886
|
+
}
|
|
7887
|
+
} catch (error) {
|
|
7888
|
+
logError(chalk6.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
7889
|
+
process.exit(1);
|
|
7890
|
+
}
|
|
7891
|
+
});
|
|
7892
|
+
program2.command("remove <id>").alias("uninstall").description("Remove a scenario (alias for delete)").option("-y, --yes", "Skip confirmation prompt", false).action(async (id, opts) => {
|
|
7893
|
+
try {
|
|
7894
|
+
const scenario = getScenario(id) ?? getScenarioByShortId(id);
|
|
7895
|
+
if (!scenario) {
|
|
7896
|
+
logError(chalk6.red(`Scenario not found: ${id}`));
|
|
7391
7897
|
process.exit(1);
|
|
7392
7898
|
}
|
|
7393
7899
|
if (!opts.yes) {
|
|
7394
|
-
process.stdout.write(
|
|
7900
|
+
process.stdout.write(chalk6.yellow(`Remove scenario ${scenario.shortId} "${scenario.name}"? [y/N] `));
|
|
7395
7901
|
const answer = await new Promise((resolve2) => {
|
|
7396
7902
|
let buf = "";
|
|
7397
7903
|
process.stdin.setRawMode?.(true);
|
|
@@ -7406,33 +7912,33 @@ program2.command("delete <id>").description("Delete a scenario").option("-y, --y
|
|
|
7406
7912
|
});
|
|
7407
7913
|
});
|
|
7408
7914
|
if (answer !== "y" && answer !== "yes") {
|
|
7409
|
-
log(
|
|
7915
|
+
log(chalk6.dim("Cancelled."));
|
|
7410
7916
|
return;
|
|
7411
7917
|
}
|
|
7412
7918
|
}
|
|
7413
7919
|
const deleted = deleteScenario(scenario.id);
|
|
7414
7920
|
if (deleted) {
|
|
7415
|
-
log(
|
|
7921
|
+
log(chalk6.green(`Removed scenario ${scenario.shortId}: ${scenario.name}`));
|
|
7416
7922
|
} else {
|
|
7417
|
-
logError(
|
|
7923
|
+
logError(chalk6.red(`Failed to remove scenario: ${id}`));
|
|
7418
7924
|
process.exit(1);
|
|
7419
7925
|
}
|
|
7420
7926
|
} catch (error) {
|
|
7421
|
-
logError(
|
|
7927
|
+
logError(chalk6.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
7422
7928
|
process.exit(1);
|
|
7423
7929
|
}
|
|
7424
7930
|
});
|
|
7425
7931
|
program2.command("run [url] [description]").alias("test").description("Run test scenarios against a URL").option("-t, --tag <tag>", "Filter by tag (repeatable)", (val, acc) => {
|
|
7426
7932
|
acc.push(val);
|
|
7427
7933
|
return acc;
|
|
7428
|
-
}, []).option("-s, --scenario <id>", "Run specific scenario ID").option("-p, --priority <level>", "Filter by priority").option("--headed", "Run browser in headed mode", false).option("-m, --model <model>", "AI model to use").option("--parallel <n>", "Number of parallel browsers", "1").option("--json", "Output results as JSON", false).option("-o, --output <filepath>", "Write JSON results to file").option("--timeout <ms>", "Timeout in milliseconds").option("--from-todos", "Import scenarios from todos before running", false).option("--project <id>", "Project ID").option("-b, --background", "Start run in background and return immediately", false).option("--browser <engine>", "Browser engine: playwright or lightpanda", "playwright").option("--env <name>", "Use a named environment for the URL").option("--dry-run", "Print what would run without launching browser", false).option("--retry <n>", "Retry failed scenarios up to n times", "0").option("--verbose", "Show per-step timing and full tool results", false).option("--watch-results", "When used with --background, poll and display live results table until run completes", false).option("--failed-only", "Only show failed/error scenarios in output (passed count shown as summary)", false).action(async (urlArg, description, opts) => {
|
|
7934
|
+
}, []).option("-s, --scenario <id>", "Run specific scenario ID").option("-p, --priority <level>", "Filter by priority").option("--headed", "Run browser in headed mode", false).option("-m, --model <model>", "AI model to use").option("--parallel <n>", "Number of parallel browsers", "1").option("--json", "Output results as JSON", false).option("-o, --output <filepath>", "Write JSON results to file").option("--timeout <ms>", "Timeout in milliseconds").option("--from-todos", "Import scenarios from todos before running", false).option("--project <id>", "Project ID").option("-b, --background", "Start run in background and return immediately", false).option("--browser <engine>", "Browser engine: playwright or lightpanda", "playwright").option("--env <name>", "Use a named environment for the URL").option("--dry-run", "Print what would run without launching browser", false).option("--retry <n>", "Retry failed scenarios up to n times", "0").option("--verbose", "Show per-step timing and full tool results", false).option("--watch-results", "When used with --background, poll and display live results table until run completes", false).option("--failed-only", "Only show failed/error scenarios in output (passed count shown as summary)", false).option("--smoke", "Run only smoke-tagged scenarios (fast validation suite, <2 min)", false).action(async (urlArg, description, opts) => {
|
|
7429
7935
|
try {
|
|
7430
7936
|
const projectId = resolveProject(opts.project);
|
|
7431
7937
|
let url = urlArg;
|
|
7432
7938
|
if (!url && opts.env) {
|
|
7433
7939
|
const env = getEnvironment(opts.env);
|
|
7434
7940
|
if (!env) {
|
|
7435
|
-
logError(
|
|
7941
|
+
logError(chalk6.red(`Environment not found: ${opts.env}`));
|
|
7436
7942
|
process.exit(1);
|
|
7437
7943
|
}
|
|
7438
7944
|
url = env.url;
|
|
@@ -7441,29 +7947,33 @@ program2.command("run [url] [description]").alias("test").description("Run test
|
|
|
7441
7947
|
const defaultEnv = getDefaultEnvironment();
|
|
7442
7948
|
if (defaultEnv) {
|
|
7443
7949
|
url = defaultEnv.url;
|
|
7444
|
-
log(
|
|
7950
|
+
log(chalk6.dim(`Using default environment: ${defaultEnv.name} (${defaultEnv.url})`));
|
|
7445
7951
|
}
|
|
7446
7952
|
}
|
|
7447
7953
|
if (!url) {
|
|
7448
|
-
logError(
|
|
7954
|
+
logError(chalk6.red("No URL provided. Pass a URL argument, use --env <name>, or set a default environment with 'testers env use <name>'."));
|
|
7449
7955
|
process.exit(1);
|
|
7450
7956
|
}
|
|
7451
7957
|
if (!opts.dryRun && !opts.background) {
|
|
7452
7958
|
const budgetResult = checkBudget(0);
|
|
7453
7959
|
if (budgetResult.warning) {
|
|
7454
|
-
log(
|
|
7960
|
+
log(chalk6.yellow(` \u26A0\uFE0F Budget warning: ${budgetResult.warning}`));
|
|
7455
7961
|
if (!budgetResult.allowed) {
|
|
7456
7962
|
if (!opts.yes) {
|
|
7457
|
-
log(
|
|
7963
|
+
log(chalk6.yellow(" Use --yes to run anyway, or check your budget config."));
|
|
7458
7964
|
process.exit(1);
|
|
7459
7965
|
}
|
|
7460
|
-
log(
|
|
7966
|
+
log(chalk6.yellow(" --yes passed, proceeding despite budget limit."));
|
|
7461
7967
|
}
|
|
7462
7968
|
}
|
|
7463
7969
|
}
|
|
7970
|
+
if (opts.smoke && !opts.tag.includes("smoke")) {
|
|
7971
|
+
opts.tag.push("smoke");
|
|
7972
|
+
log(chalk6.dim(" Running smoke suite (scenarios tagged 'smoke')..."));
|
|
7973
|
+
}
|
|
7464
7974
|
if (opts.fromTodos) {
|
|
7465
7975
|
const result = importFromTodos({ projectId });
|
|
7466
|
-
log(
|
|
7976
|
+
log(chalk6.blue(`Imported ${result.imported} scenarios from todos (${result.skipped} skipped)`));
|
|
7467
7977
|
}
|
|
7468
7978
|
if (opts.dryRun) {
|
|
7469
7979
|
const dryScenarios = listScenarios({
|
|
@@ -7477,10 +7987,10 @@ program2.command("run [url] [description]").alias("test").description("Run test
|
|
|
7477
7987
|
return true;
|
|
7478
7988
|
});
|
|
7479
7989
|
log("");
|
|
7480
|
-
log(
|
|
7990
|
+
log(chalk6.bold(" Dry Run \u2014 scenarios that would execute:"));
|
|
7481
7991
|
log("");
|
|
7482
7992
|
if (dryScenarios.length === 0) {
|
|
7483
|
-
log(
|
|
7993
|
+
log(chalk6.yellow(" No matching scenarios found."));
|
|
7484
7994
|
} else {
|
|
7485
7995
|
for (const s of dryScenarios) {
|
|
7486
7996
|
const assertionErrors = [];
|
|
@@ -7496,19 +8006,19 @@ program2.command("run [url] [description]").alias("test").description("Run test
|
|
|
7496
8006
|
const presets = listAuthPresets();
|
|
7497
8007
|
authOk = presets.some((p) => p.name === s.authPreset);
|
|
7498
8008
|
}
|
|
7499
|
-
const statusIcon = assertionErrors.length === 0 && authOk ?
|
|
7500
|
-
log(` ${statusIcon} ${
|
|
8009
|
+
const statusIcon = assertionErrors.length === 0 && authOk ? chalk6.green("\u2713") : chalk6.red("\u2717");
|
|
8010
|
+
log(` ${statusIcon} ${chalk6.cyan(s.shortId)} ${s.name} ${chalk6.dim(`[${s.tags.join(", ")}]`)}`);
|
|
7501
8011
|
if (assertionErrors.length > 0) {
|
|
7502
|
-
log(
|
|
8012
|
+
log(chalk6.red(` Invalid assertions: ${assertionErrors.join(", ")}`));
|
|
7503
8013
|
}
|
|
7504
8014
|
if (!authOk) {
|
|
7505
|
-
log(
|
|
8015
|
+
log(chalk6.red(` Auth preset not found: ${s.authPreset}`));
|
|
7506
8016
|
}
|
|
7507
8017
|
}
|
|
7508
8018
|
}
|
|
7509
8019
|
log("");
|
|
7510
|
-
log(
|
|
7511
|
-
log(
|
|
8020
|
+
log(chalk6.dim(` URL: ${url}`));
|
|
8021
|
+
log(chalk6.dim(` Total: ${dryScenarios.length} scenarios`));
|
|
7512
8022
|
log("");
|
|
7513
8023
|
process.exit(0);
|
|
7514
8024
|
}
|
|
@@ -7528,11 +8038,11 @@ program2.command("run [url] [description]").alias("test").description("Run test
|
|
|
7528
8038
|
projectId,
|
|
7529
8039
|
engine: opts.browser
|
|
7530
8040
|
});
|
|
7531
|
-
log(
|
|
7532
|
-
log(
|
|
7533
|
-
log(
|
|
8041
|
+
log(chalk6.green(`Run started in background: ${chalk6.bold(runId.slice(0, 8))}`));
|
|
8042
|
+
log(chalk6.dim(` Scenarios: ${scenarioCount}`));
|
|
8043
|
+
log(chalk6.dim(` URL: ${url}`));
|
|
7534
8044
|
if (opts.watchResults) {
|
|
7535
|
-
log(
|
|
8045
|
+
log(chalk6.dim(` Watching results (polling every 3s)...`));
|
|
7536
8046
|
log("");
|
|
7537
8047
|
const POLL_INTERVAL = 3000;
|
|
7538
8048
|
const DONE_STATUSES = new Set(["passed", "failed", "cancelled"]);
|
|
@@ -7541,14 +8051,14 @@ program2.command("run [url] [description]").alias("test").description("Run test
|
|
|
7541
8051
|
if (!run2)
|
|
7542
8052
|
return;
|
|
7543
8053
|
const results2 = getResultsByRun(runId);
|
|
7544
|
-
const statusIcon = run2.status === "passed" ?
|
|
8054
|
+
const statusIcon = run2.status === "passed" ? chalk6.green("PASS") : run2.status === "failed" ? chalk6.red("FAIL") : chalk6.blue("RUN ");
|
|
7545
8055
|
process.stdout.write(`\r ${statusIcon} ${run2.passed} passed ${run2.failed} failed ${run2.total - run2.passed - run2.failed} running (${results2.length}/${run2.total})
|
|
7546
8056
|
`);
|
|
7547
8057
|
for (const r of results2) {
|
|
7548
8058
|
const scenario = getScenario(r.scenarioId);
|
|
7549
8059
|
const name = scenario ? scenario.name : r.scenarioId.slice(0, 8);
|
|
7550
|
-
const icon = r.status === "passed" ?
|
|
7551
|
-
const dur = r.durationMs > 0 ?
|
|
8060
|
+
const icon = r.status === "passed" ? chalk6.green("\u2713") : r.status === "failed" ? chalk6.red("\u2717") : r.status === "error" ? chalk6.yellow("!") : chalk6.blue("\u2026");
|
|
8061
|
+
const dur = r.durationMs > 0 ? chalk6.dim(` ${(r.durationMs / 1000).toFixed(1)}s`) : "";
|
|
7552
8062
|
process.stdout.write(` ${icon} ${name}${dur}
|
|
7553
8063
|
`);
|
|
7554
8064
|
}
|
|
@@ -7573,7 +8083,7 @@ program2.command("run [url] [description]").alias("test").description("Run test
|
|
|
7573
8083
|
}
|
|
7574
8084
|
process.exit(finalRun ? getExitCode(finalRun) : 0);
|
|
7575
8085
|
}
|
|
7576
|
-
log(
|
|
8086
|
+
log(chalk6.dim(` Check progress: testers results ${runId.slice(0, 8)}`));
|
|
7577
8087
|
process.exit(0);
|
|
7578
8088
|
}
|
|
7579
8089
|
if (!opts.json && !opts.output) {
|
|
@@ -7582,51 +8092,51 @@ program2.command("run [url] [description]").alias("test").description("Run test
|
|
|
7582
8092
|
switch (event.type) {
|
|
7583
8093
|
case "scenario:start":
|
|
7584
8094
|
if (event.retryAttempt) {
|
|
7585
|
-
log(
|
|
8095
|
+
log(chalk6.yellow(` [retry] Retrying scenario ${event.scenarioName ?? event.scenarioId} (attempt ${event.retryAttempt}/${event.maxRetries})...`));
|
|
7586
8096
|
} else {
|
|
7587
|
-
log(
|
|
8097
|
+
log(chalk6.blue(` [start] ${event.scenarioName ?? event.scenarioId}`));
|
|
7588
8098
|
}
|
|
7589
8099
|
break;
|
|
7590
8100
|
case "scenario:timeout_warning": {
|
|
7591
8101
|
const elapsedS = ((event.elapsedMs ?? 0) / 1000).toFixed(0);
|
|
7592
8102
|
const totalS = ((event.timeoutMs ?? 0) / 1000).toFixed(0);
|
|
7593
|
-
log(
|
|
8103
|
+
log(chalk6.yellow(` \u26A0\uFE0F Scenario '${event.scenarioName}' at 80% timeout (${elapsedS}s/${totalS}s) \u2014 still running`));
|
|
7594
8104
|
break;
|
|
7595
8105
|
}
|
|
7596
8106
|
case "step:thinking":
|
|
7597
8107
|
if (event.thinking) {
|
|
7598
8108
|
const preview = event.thinking.length > 120 ? event.thinking.slice(0, 120) + "..." : event.thinking;
|
|
7599
|
-
log(
|
|
8109
|
+
log(chalk6.dim(` [think] ${preview}`));
|
|
7600
8110
|
}
|
|
7601
8111
|
break;
|
|
7602
8112
|
case "step:tool_call":
|
|
7603
|
-
log(
|
|
8113
|
+
log(chalk6.cyan(` [step ${event.stepNumber}] ${event.toolName}${event.toolInput ? ` ${formatToolInput(event.toolInput)}` : ""}`));
|
|
7604
8114
|
break;
|
|
7605
8115
|
case "step:tool_result":
|
|
7606
8116
|
if (event.toolName === "report_result") {
|
|
7607
|
-
log(
|
|
8117
|
+
log(chalk6.bold(` [result] ${event.toolResult}`));
|
|
7608
8118
|
} else {
|
|
7609
|
-
const durationStr = verbose && event.stepDurationMs !== undefined ?
|
|
8119
|
+
const durationStr = verbose && event.stepDurationMs !== undefined ? chalk6.dim(`[${(event.stepDurationMs / 1000).toFixed(1)}s] `) : "";
|
|
7610
8120
|
const resultPreview = (event.toolResult ?? "").length > 100 ? (event.toolResult ?? "").slice(0, 100) + "..." : event.toolResult ?? "";
|
|
7611
|
-
log(
|
|
8121
|
+
log(chalk6.dim(` [done] ${durationStr}${resultPreview}`));
|
|
7612
8122
|
}
|
|
7613
8123
|
break;
|
|
7614
8124
|
case "screenshot:captured":
|
|
7615
|
-
log(
|
|
8125
|
+
log(chalk6.dim(` [screenshot] ${event.screenshotPath}`));
|
|
7616
8126
|
break;
|
|
7617
8127
|
case "scenario:pass":
|
|
7618
|
-
log(
|
|
8128
|
+
log(chalk6.green(` [PASS] ${event.scenarioName}`));
|
|
7619
8129
|
break;
|
|
7620
8130
|
case "scenario:fail":
|
|
7621
|
-
log(
|
|
8131
|
+
log(chalk6.red(` [FAIL] ${event.scenarioName}`));
|
|
7622
8132
|
break;
|
|
7623
8133
|
case "scenario:error":
|
|
7624
|
-
log(
|
|
8134
|
+
log(chalk6.yellow(` [ERR] ${event.scenarioName}: ${event.error}`));
|
|
7625
8135
|
break;
|
|
7626
8136
|
}
|
|
7627
8137
|
});
|
|
7628
8138
|
log("");
|
|
7629
|
-
log(
|
|
8139
|
+
log(chalk6.bold(` Running tests against ${url}`));
|
|
7630
8140
|
log("");
|
|
7631
8141
|
}
|
|
7632
8142
|
if (description) {
|
|
@@ -7651,7 +8161,7 @@ program2.command("run [url] [description]").alias("test").description("Run test
|
|
|
7651
8161
|
const jsonOutput = formatJSON(run2, results2);
|
|
7652
8162
|
if (opts.output) {
|
|
7653
8163
|
writeFileSync3(opts.output, jsonOutput, "utf-8");
|
|
7654
|
-
log(
|
|
8164
|
+
log(chalk6.green(`Results written to ${opts.output}`));
|
|
7655
8165
|
}
|
|
7656
8166
|
if (opts.json) {
|
|
7657
8167
|
log(jsonOutput);
|
|
@@ -7664,7 +8174,7 @@ program2.command("run [url] [description]").alias("test").description("Run test
|
|
|
7664
8174
|
const noFilters = !opts.scenario && opts.tag.length === 0 && !opts.priority;
|
|
7665
8175
|
if (noFilters && !opts.json && !opts.output) {
|
|
7666
8176
|
const allScenarios = listScenarios({ projectId });
|
|
7667
|
-
log(
|
|
8177
|
+
log(chalk6.bold(` Running all ${allScenarios.length} scenarios...`));
|
|
7668
8178
|
log("");
|
|
7669
8179
|
}
|
|
7670
8180
|
const { run, results } = await runByFilter({
|
|
@@ -7684,7 +8194,7 @@ program2.command("run [url] [description]").alias("test").description("Run test
|
|
|
7684
8194
|
const jsonOutput = formatJSON(run, results);
|
|
7685
8195
|
if (opts.output) {
|
|
7686
8196
|
writeFileSync3(opts.output, jsonOutput, "utf-8");
|
|
7687
|
-
log(
|
|
8197
|
+
log(chalk6.green(`Results written to ${opts.output}`));
|
|
7688
8198
|
}
|
|
7689
8199
|
if (opts.json) {
|
|
7690
8200
|
log(jsonOutput);
|
|
@@ -7694,7 +8204,7 @@ program2.command("run [url] [description]").alias("test").description("Run test
|
|
|
7694
8204
|
}
|
|
7695
8205
|
process.exit(getExitCode(run));
|
|
7696
8206
|
} catch (error) {
|
|
7697
|
-
logError(
|
|
8207
|
+
logError(chalk6.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
7698
8208
|
process.exit(1);
|
|
7699
8209
|
}
|
|
7700
8210
|
});
|
|
@@ -7713,7 +8223,7 @@ program2.command("runs").description("List past test runs").option("--status <st
|
|
|
7713
8223
|
log(formatRunList(runs));
|
|
7714
8224
|
}
|
|
7715
8225
|
} catch (error) {
|
|
7716
|
-
logError(
|
|
8226
|
+
logError(chalk6.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
7717
8227
|
process.exit(1);
|
|
7718
8228
|
}
|
|
7719
8229
|
});
|
|
@@ -7721,7 +8231,7 @@ program2.command("results <run-id>").description("Show results for a test run").
|
|
|
7721
8231
|
try {
|
|
7722
8232
|
const run = getRun(runId);
|
|
7723
8233
|
if (!run) {
|
|
7724
|
-
logError(
|
|
8234
|
+
logError(chalk6.red(`Run not found: ${runId}`));
|
|
7725
8235
|
process.exit(1);
|
|
7726
8236
|
}
|
|
7727
8237
|
const results = getResultsByRun(run.id);
|
|
@@ -7731,7 +8241,7 @@ program2.command("results <run-id>").description("Show results for a test run").
|
|
|
7731
8241
|
log(formatTerminal(run, results));
|
|
7732
8242
|
}
|
|
7733
8243
|
} catch (error) {
|
|
7734
|
-
logError(
|
|
8244
|
+
logError(chalk6.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
7735
8245
|
process.exit(1);
|
|
7736
8246
|
}
|
|
7737
8247
|
});
|
|
@@ -7742,23 +8252,23 @@ program2.command("screenshots <id>").description("List screenshots for a run or
|
|
|
7742
8252
|
const results = getResultsByRun(run.id);
|
|
7743
8253
|
let total = 0;
|
|
7744
8254
|
log("");
|
|
7745
|
-
log(
|
|
8255
|
+
log(chalk6.bold(` Screenshots for run ${run.id.slice(0, 8)}`));
|
|
7746
8256
|
log("");
|
|
7747
8257
|
for (const result of results) {
|
|
7748
8258
|
const screenshots2 = listScreenshots(result.id);
|
|
7749
8259
|
if (screenshots2.length > 0) {
|
|
7750
8260
|
const scenario = getScenario(result.scenarioId);
|
|
7751
8261
|
const label = scenario ? `${scenario.shortId}: ${scenario.name}` : result.scenarioId.slice(0, 8);
|
|
7752
|
-
log(
|
|
8262
|
+
log(chalk6.bold(` ${label}`));
|
|
7753
8263
|
for (const ss of screenshots2) {
|
|
7754
|
-
log(` ${
|
|
8264
|
+
log(` ${chalk6.dim(String(ss.stepNumber).padStart(3, "0"))} ${ss.action} \u2014 ${chalk6.dim(ss.filePath)}`);
|
|
7755
8265
|
total++;
|
|
7756
8266
|
}
|
|
7757
8267
|
log("");
|
|
7758
8268
|
}
|
|
7759
8269
|
}
|
|
7760
8270
|
if (total === 0) {
|
|
7761
|
-
log(
|
|
8271
|
+
log(chalk6.dim(" No screenshots found."));
|
|
7762
8272
|
log("");
|
|
7763
8273
|
}
|
|
7764
8274
|
return;
|
|
@@ -7766,18 +8276,18 @@ program2.command("screenshots <id>").description("List screenshots for a run or
|
|
|
7766
8276
|
const screenshots = listScreenshots(id);
|
|
7767
8277
|
if (screenshots.length > 0) {
|
|
7768
8278
|
log("");
|
|
7769
|
-
log(
|
|
8279
|
+
log(chalk6.bold(` Screenshots for result ${id.slice(0, 8)}`));
|
|
7770
8280
|
log("");
|
|
7771
8281
|
for (const ss of screenshots) {
|
|
7772
|
-
log(` ${
|
|
8282
|
+
log(` ${chalk6.dim(String(ss.stepNumber).padStart(3, "0"))} ${ss.action} \u2014 ${chalk6.dim(ss.filePath)}`);
|
|
7773
8283
|
}
|
|
7774
8284
|
log("");
|
|
7775
8285
|
return;
|
|
7776
8286
|
}
|
|
7777
|
-
logError(
|
|
8287
|
+
logError(chalk6.red(`No screenshots found for: ${id}`));
|
|
7778
8288
|
process.exit(1);
|
|
7779
8289
|
} catch (error) {
|
|
7780
|
-
logError(
|
|
8290
|
+
logError(chalk6.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
7781
8291
|
process.exit(1);
|
|
7782
8292
|
}
|
|
7783
8293
|
});
|
|
@@ -7786,7 +8296,7 @@ program2.command("import <dir>").description("Import markdown test files as scen
|
|
|
7786
8296
|
const absDir = resolve(dir);
|
|
7787
8297
|
const files = readdirSync(absDir).filter((f) => f.endsWith(".md"));
|
|
7788
8298
|
if (files.length === 0) {
|
|
7789
|
-
log(
|
|
8299
|
+
log(chalk6.dim("No .md files found in directory."));
|
|
7790
8300
|
return;
|
|
7791
8301
|
}
|
|
7792
8302
|
let imported = 0;
|
|
@@ -7816,13 +8326,13 @@ program2.command("import <dir>").description("Import markdown test files as scen
|
|
|
7816
8326
|
description: descriptionLines.join(" ") || name,
|
|
7817
8327
|
steps
|
|
7818
8328
|
});
|
|
7819
|
-
log(
|
|
8329
|
+
log(chalk6.green(` Imported ${chalk6.bold(scenario.shortId)}: ${scenario.name}`));
|
|
7820
8330
|
imported++;
|
|
7821
8331
|
}
|
|
7822
8332
|
log("");
|
|
7823
|
-
log(
|
|
8333
|
+
log(chalk6.green(`Imported ${imported} scenario(s) from ${absDir}`));
|
|
7824
8334
|
} catch (error) {
|
|
7825
|
-
logError(
|
|
8335
|
+
logError(chalk6.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
7826
8336
|
process.exit(1);
|
|
7827
8337
|
}
|
|
7828
8338
|
});
|
|
@@ -7830,7 +8340,7 @@ program2.command("export [format]").description("Export scenarios as JSON (defau
|
|
|
7830
8340
|
try {
|
|
7831
8341
|
const fmt = (format ?? "json").toLowerCase();
|
|
7832
8342
|
if (fmt !== "json" && fmt !== "markdown") {
|
|
7833
|
-
logError(
|
|
8343
|
+
logError(chalk6.red(`Unknown format: ${fmt}. Supported: json, markdown`));
|
|
7834
8344
|
process.exit(1);
|
|
7835
8345
|
}
|
|
7836
8346
|
const projectId = resolveProject(opts.project);
|
|
@@ -7840,14 +8350,14 @@ program2.command("export [format]").description("Export scenarios as JSON (defau
|
|
|
7840
8350
|
projectId
|
|
7841
8351
|
});
|
|
7842
8352
|
if (scenarios.length === 0) {
|
|
7843
|
-
log(
|
|
8353
|
+
log(chalk6.dim("No scenarios found to export."));
|
|
7844
8354
|
return;
|
|
7845
8355
|
}
|
|
7846
8356
|
if (fmt === "json") {
|
|
7847
8357
|
const outputPath = opts.output ?? "testers-export.json";
|
|
7848
8358
|
const data = JSON.stringify(scenarios, null, 2);
|
|
7849
8359
|
writeFileSync3(outputPath, data, "utf-8");
|
|
7850
|
-
log(
|
|
8360
|
+
log(chalk6.green(`Exported ${scenarios.length} scenario(s) to ${resolve(outputPath)}`));
|
|
7851
8361
|
return;
|
|
7852
8362
|
}
|
|
7853
8363
|
const outputDir = opts.output ?? ".";
|
|
@@ -7882,12 +8392,12 @@ program2.command("export [format]").description("Export scenarios as JSON (defau
|
|
|
7882
8392
|
const filePath = join6(outputDir, `${s.shortId}-${safeFilename}.md`);
|
|
7883
8393
|
writeFileSync3(filePath, lines.join(`
|
|
7884
8394
|
`), "utf-8");
|
|
7885
|
-
log(
|
|
8395
|
+
log(chalk6.dim(` ${s.shortId}: ${s.name} \u2192 ${filePath}`));
|
|
7886
8396
|
}
|
|
7887
|
-
log(
|
|
8397
|
+
log(chalk6.green(`
|
|
7888
8398
|
Exported ${scenarios.length} scenario(s) as markdown to ${resolve(outputDir)}`));
|
|
7889
8399
|
} catch (error) {
|
|
7890
|
-
logError(
|
|
8400
|
+
logError(chalk6.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
7891
8401
|
process.exit(1);
|
|
7892
8402
|
}
|
|
7893
8403
|
});
|
|
@@ -7896,7 +8406,7 @@ program2.command("config").description("Show current configuration").action(() =
|
|
|
7896
8406
|
const config = loadConfig();
|
|
7897
8407
|
log(JSON.stringify(config, null, 2));
|
|
7898
8408
|
} catch (error) {
|
|
7899
|
-
logError(
|
|
8409
|
+
logError(chalk6.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
7900
8410
|
process.exit(1);
|
|
7901
8411
|
}
|
|
7902
8412
|
});
|
|
@@ -7906,32 +8416,32 @@ program2.command("status").description("Show database and auth status").action((
|
|
|
7906
8416
|
const hasApiKey = !!config.anthropicApiKey || !!process.env["ANTHROPIC_API_KEY"];
|
|
7907
8417
|
const dbPath = join6(process.env["HOME"] ?? "~", ".testers", "testers.db");
|
|
7908
8418
|
log("");
|
|
7909
|
-
log(
|
|
8419
|
+
log(chalk6.bold(" Open Testers Status"));
|
|
7910
8420
|
log("");
|
|
7911
|
-
log(` ANTHROPIC_API_KEY: ${hasApiKey ?
|
|
8421
|
+
log(` ANTHROPIC_API_KEY: ${hasApiKey ? chalk6.green("set") : chalk6.red("not set")}`);
|
|
7912
8422
|
log(` Database: ${dbPath}`);
|
|
7913
8423
|
log(` Default model: ${config.defaultModel}`);
|
|
7914
8424
|
log(` Screenshots dir: ${config.screenshots.dir}`);
|
|
7915
8425
|
log("");
|
|
7916
8426
|
} catch (error) {
|
|
7917
|
-
logError(
|
|
8427
|
+
logError(chalk6.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
7918
8428
|
process.exit(1);
|
|
7919
8429
|
}
|
|
7920
8430
|
});
|
|
7921
8431
|
program2.command("install-browser").description("Install browser engine").option("--engine <engine>", "Engine to install: playwright, lightpanda, or all", "playwright").action(async (opts) => {
|
|
7922
8432
|
try {
|
|
7923
8433
|
if (opts.engine === "all" || opts.engine === "playwright") {
|
|
7924
|
-
log(
|
|
8434
|
+
log(chalk6.blue("Installing Playwright Chromium..."));
|
|
7925
8435
|
await installBrowser("playwright");
|
|
7926
|
-
log(
|
|
8436
|
+
log(chalk6.green("Playwright Chromium installed."));
|
|
7927
8437
|
}
|
|
7928
8438
|
if (opts.engine === "all" || opts.engine === "lightpanda") {
|
|
7929
|
-
log(
|
|
8439
|
+
log(chalk6.blue("Installing Lightpanda..."));
|
|
7930
8440
|
await installBrowser("lightpanda");
|
|
7931
|
-
log(
|
|
8441
|
+
log(chalk6.green("Lightpanda installed."));
|
|
7932
8442
|
}
|
|
7933
8443
|
} catch (error) {
|
|
7934
|
-
logError(
|
|
8444
|
+
logError(chalk6.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
7935
8445
|
process.exit(1);
|
|
7936
8446
|
}
|
|
7937
8447
|
});
|
|
@@ -7943,9 +8453,9 @@ projectCmd.command("create <name>").description("Create a new project").option("
|
|
|
7943
8453
|
path: opts.path,
|
|
7944
8454
|
description: opts.description
|
|
7945
8455
|
});
|
|
7946
|
-
log(
|
|
8456
|
+
log(chalk6.green(`Created project ${chalk6.bold(project.name)} (${project.id})`));
|
|
7947
8457
|
} catch (error) {
|
|
7948
|
-
logError(
|
|
8458
|
+
logError(chalk6.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
7949
8459
|
process.exit(1);
|
|
7950
8460
|
}
|
|
7951
8461
|
});
|
|
@@ -7953,20 +8463,20 @@ projectCmd.command("list").description("List all projects").action(() => {
|
|
|
7953
8463
|
try {
|
|
7954
8464
|
const projects = listProjects();
|
|
7955
8465
|
if (projects.length === 0) {
|
|
7956
|
-
log(
|
|
8466
|
+
log(chalk6.dim("No projects found."));
|
|
7957
8467
|
return;
|
|
7958
8468
|
}
|
|
7959
8469
|
log("");
|
|
7960
|
-
log(
|
|
8470
|
+
log(chalk6.bold(" Projects"));
|
|
7961
8471
|
log("");
|
|
7962
8472
|
log(` ${"ID".padEnd(38)} ${"Name".padEnd(24)} ${"Path".padEnd(30)} Created`);
|
|
7963
8473
|
log(` ${"\u2500".repeat(38)} ${"\u2500".repeat(24)} ${"\u2500".repeat(30)} ${"\u2500".repeat(20)}`);
|
|
7964
8474
|
for (const p of projects) {
|
|
7965
|
-
log(` ${p.id.padEnd(38)} ${p.name.padEnd(24)} ${(p.path ??
|
|
8475
|
+
log(` ${p.id.padEnd(38)} ${p.name.padEnd(24)} ${(p.path ?? chalk6.dim("\u2014")).toString().padEnd(30)} ${p.createdAt}`);
|
|
7966
8476
|
}
|
|
7967
8477
|
log("");
|
|
7968
8478
|
} catch (error) {
|
|
7969
|
-
logError(
|
|
8479
|
+
logError(chalk6.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
7970
8480
|
process.exit(1);
|
|
7971
8481
|
}
|
|
7972
8482
|
});
|
|
@@ -7974,19 +8484,19 @@ projectCmd.command("show <id>").description("Show project details").action((id)
|
|
|
7974
8484
|
try {
|
|
7975
8485
|
const project = getProject(id);
|
|
7976
8486
|
if (!project) {
|
|
7977
|
-
logError(
|
|
8487
|
+
logError(chalk6.red(`Project not found: ${id}`));
|
|
7978
8488
|
process.exit(1);
|
|
7979
8489
|
}
|
|
7980
8490
|
log("");
|
|
7981
|
-
log(
|
|
8491
|
+
log(chalk6.bold(` Project: ${project.name}`));
|
|
7982
8492
|
log(` ID: ${project.id}`);
|
|
7983
|
-
log(` Path: ${project.path ??
|
|
7984
|
-
log(` Description: ${project.description ??
|
|
8493
|
+
log(` Path: ${project.path ?? chalk6.dim("none")}`);
|
|
8494
|
+
log(` Description: ${project.description ?? chalk6.dim("none")}`);
|
|
7985
8495
|
log(` Created: ${project.createdAt}`);
|
|
7986
8496
|
log(` Updated: ${project.updatedAt}`);
|
|
7987
8497
|
log("");
|
|
7988
8498
|
} catch (error) {
|
|
7989
|
-
logError(
|
|
8499
|
+
logError(chalk6.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
7990
8500
|
process.exit(1);
|
|
7991
8501
|
}
|
|
7992
8502
|
});
|
|
@@ -8004,9 +8514,9 @@ projectCmd.command("use <name>").description("Set active project (find or create
|
|
|
8004
8514
|
}
|
|
8005
8515
|
config.activeProject = project.id;
|
|
8006
8516
|
writeFileSync3(CONFIG_PATH2, JSON.stringify(config, null, 2), "utf-8");
|
|
8007
|
-
log(
|
|
8517
|
+
log(chalk6.green(`Active project set to ${chalk6.bold(project.name)} (${project.id})`));
|
|
8008
8518
|
} catch (error) {
|
|
8009
|
-
logError(
|
|
8519
|
+
logError(chalk6.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
8010
8520
|
process.exit(1);
|
|
8011
8521
|
}
|
|
8012
8522
|
});
|
|
@@ -8031,12 +8541,12 @@ scheduleCmd.command("create <name>").description("Create a new schedule").requir
|
|
|
8031
8541
|
timeoutMs: opts.timeout ? parseInt(opts.timeout, 10) : undefined,
|
|
8032
8542
|
projectId
|
|
8033
8543
|
});
|
|
8034
|
-
log(
|
|
8544
|
+
log(chalk6.green(`Created schedule ${chalk6.bold(schedule.name)} (${schedule.id})`));
|
|
8035
8545
|
if (schedule.nextRunAt) {
|
|
8036
|
-
log(
|
|
8546
|
+
log(chalk6.dim(` Next run at: ${schedule.nextRunAt}`));
|
|
8037
8547
|
}
|
|
8038
8548
|
} catch (error) {
|
|
8039
|
-
logError(
|
|
8549
|
+
logError(chalk6.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
8040
8550
|
process.exit(1);
|
|
8041
8551
|
}
|
|
8042
8552
|
});
|
|
@@ -8052,23 +8562,23 @@ scheduleCmd.command("list").description("List schedules").option("--project <id>
|
|
|
8052
8562
|
return;
|
|
8053
8563
|
}
|
|
8054
8564
|
if (schedules.length === 0) {
|
|
8055
|
-
log(
|
|
8565
|
+
log(chalk6.dim("No schedules found."));
|
|
8056
8566
|
return;
|
|
8057
8567
|
}
|
|
8058
8568
|
log("");
|
|
8059
|
-
log(
|
|
8569
|
+
log(chalk6.bold(" Schedules"));
|
|
8060
8570
|
log("");
|
|
8061
8571
|
log(` ${"Name".padEnd(20)} ${"Cron".padEnd(18)} ${"URL".padEnd(30)} ${"Enabled".padEnd(9)} ${"Next Run".padEnd(22)} Last Run`);
|
|
8062
8572
|
log(` ${"\u2500".repeat(20)} ${"\u2500".repeat(18)} ${"\u2500".repeat(30)} ${"\u2500".repeat(9)} ${"\u2500".repeat(22)} ${"\u2500".repeat(22)}`);
|
|
8063
8573
|
for (const s of schedules) {
|
|
8064
|
-
const enabled = s.enabled ?
|
|
8065
|
-
const nextRun = s.nextRunAt ??
|
|
8066
|
-
const lastRun = s.lastRunAt ??
|
|
8574
|
+
const enabled = s.enabled ? chalk6.green("yes") : chalk6.red("no");
|
|
8575
|
+
const nextRun = s.nextRunAt ?? chalk6.dim("\u2014");
|
|
8576
|
+
const lastRun = s.lastRunAt ?? chalk6.dim("\u2014");
|
|
8067
8577
|
log(` ${s.name.padEnd(20)} ${s.cronExpression.padEnd(18)} ${s.url.padEnd(30)} ${enabled.toString().padEnd(9)} ${nextRun.toString().padEnd(22)} ${lastRun}`);
|
|
8068
8578
|
}
|
|
8069
8579
|
log("");
|
|
8070
8580
|
} catch (error) {
|
|
8071
|
-
logError(
|
|
8581
|
+
logError(chalk6.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
8072
8582
|
process.exit(1);
|
|
8073
8583
|
}
|
|
8074
8584
|
});
|
|
@@ -8076,47 +8586,47 @@ scheduleCmd.command("show <id>").description("Show schedule details").action((id
|
|
|
8076
8586
|
try {
|
|
8077
8587
|
const schedule = getSchedule(id);
|
|
8078
8588
|
if (!schedule) {
|
|
8079
|
-
logError(
|
|
8589
|
+
logError(chalk6.red(`Schedule not found: ${id}`));
|
|
8080
8590
|
process.exit(1);
|
|
8081
8591
|
}
|
|
8082
8592
|
log("");
|
|
8083
|
-
log(
|
|
8593
|
+
log(chalk6.bold(` Schedule: ${schedule.name}`));
|
|
8084
8594
|
log(` ID: ${schedule.id}`);
|
|
8085
8595
|
log(` Cron: ${schedule.cronExpression}`);
|
|
8086
8596
|
log(` URL: ${schedule.url}`);
|
|
8087
|
-
log(` Enabled: ${schedule.enabled ?
|
|
8088
|
-
log(` Model: ${schedule.model ??
|
|
8597
|
+
log(` Enabled: ${schedule.enabled ? chalk6.green("yes") : chalk6.red("no")}`);
|
|
8598
|
+
log(` Model: ${schedule.model ?? chalk6.dim("default")}`);
|
|
8089
8599
|
log(` Headed: ${schedule.headed ? "yes" : "no"}`);
|
|
8090
8600
|
log(` Parallel: ${schedule.parallel}`);
|
|
8091
|
-
log(` Timeout: ${schedule.timeoutMs ? `${schedule.timeoutMs}ms` :
|
|
8092
|
-
log(` Project: ${schedule.projectId ??
|
|
8601
|
+
log(` Timeout: ${schedule.timeoutMs ? `${schedule.timeoutMs}ms` : chalk6.dim("default")}`);
|
|
8602
|
+
log(` Project: ${schedule.projectId ?? chalk6.dim("none")}`);
|
|
8093
8603
|
log(` Filter: ${JSON.stringify(schedule.scenarioFilter)}`);
|
|
8094
|
-
log(` Next run: ${schedule.nextRunAt ??
|
|
8095
|
-
log(` Last run: ${schedule.lastRunAt ??
|
|
8096
|
-
log(` Last run ID: ${schedule.lastRunId ??
|
|
8604
|
+
log(` Next run: ${schedule.nextRunAt ?? chalk6.dim("not scheduled")}`);
|
|
8605
|
+
log(` Last run: ${schedule.lastRunAt ?? chalk6.dim("never")}`);
|
|
8606
|
+
log(` Last run ID: ${schedule.lastRunId ?? chalk6.dim("none")}`);
|
|
8097
8607
|
log(` Created: ${schedule.createdAt}`);
|
|
8098
8608
|
log(` Updated: ${schedule.updatedAt}`);
|
|
8099
8609
|
log("");
|
|
8100
8610
|
} catch (error) {
|
|
8101
|
-
logError(
|
|
8611
|
+
logError(chalk6.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
8102
8612
|
process.exit(1);
|
|
8103
8613
|
}
|
|
8104
8614
|
});
|
|
8105
8615
|
scheduleCmd.command("enable <id>").description("Enable a schedule").action((id) => {
|
|
8106
8616
|
try {
|
|
8107
8617
|
const schedule = updateSchedule(id, { enabled: true });
|
|
8108
|
-
log(
|
|
8618
|
+
log(chalk6.green(`Enabled schedule ${chalk6.bold(schedule.name)}`));
|
|
8109
8619
|
} catch (error) {
|
|
8110
|
-
logError(
|
|
8620
|
+
logError(chalk6.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
8111
8621
|
process.exit(1);
|
|
8112
8622
|
}
|
|
8113
8623
|
});
|
|
8114
8624
|
scheduleCmd.command("disable <id>").description("Disable a schedule").action((id) => {
|
|
8115
8625
|
try {
|
|
8116
8626
|
const schedule = updateSchedule(id, { enabled: false });
|
|
8117
|
-
log(
|
|
8627
|
+
log(chalk6.green(`Disabled schedule ${chalk6.bold(schedule.name)}`));
|
|
8118
8628
|
} catch (error) {
|
|
8119
|
-
logError(
|
|
8629
|
+
logError(chalk6.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
8120
8630
|
process.exit(1);
|
|
8121
8631
|
}
|
|
8122
8632
|
});
|
|
@@ -8124,13 +8634,13 @@ scheduleCmd.command("delete <id>").description("Delete a schedule").action((id)
|
|
|
8124
8634
|
try {
|
|
8125
8635
|
const deleted = deleteSchedule(id);
|
|
8126
8636
|
if (deleted) {
|
|
8127
|
-
log(
|
|
8637
|
+
log(chalk6.green(`Deleted schedule: ${id}`));
|
|
8128
8638
|
} else {
|
|
8129
|
-
logError(
|
|
8639
|
+
logError(chalk6.red(`Schedule not found: ${id}`));
|
|
8130
8640
|
process.exit(1);
|
|
8131
8641
|
}
|
|
8132
8642
|
} catch (error) {
|
|
8133
|
-
logError(
|
|
8643
|
+
logError(chalk6.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
8134
8644
|
process.exit(1);
|
|
8135
8645
|
}
|
|
8136
8646
|
});
|
|
@@ -8138,11 +8648,11 @@ scheduleCmd.command("run <id>").description("Manually trigger a schedule").optio
|
|
|
8138
8648
|
try {
|
|
8139
8649
|
const schedule = getSchedule(id);
|
|
8140
8650
|
if (!schedule) {
|
|
8141
|
-
logError(
|
|
8651
|
+
logError(chalk6.red(`Schedule not found: ${id}`));
|
|
8142
8652
|
process.exit(1);
|
|
8143
8653
|
return;
|
|
8144
8654
|
}
|
|
8145
|
-
log(
|
|
8655
|
+
log(chalk6.blue(`Running schedule ${chalk6.bold(schedule.name)} against ${schedule.url}...`));
|
|
8146
8656
|
const { run, results } = await runByFilter({
|
|
8147
8657
|
url: schedule.url,
|
|
8148
8658
|
tags: schedule.scenarioFilter.tags,
|
|
@@ -8161,15 +8671,15 @@ scheduleCmd.command("run <id>").description("Manually trigger a schedule").optio
|
|
|
8161
8671
|
}
|
|
8162
8672
|
process.exit(getExitCode(run));
|
|
8163
8673
|
} catch (error) {
|
|
8164
|
-
logError(
|
|
8674
|
+
logError(chalk6.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
8165
8675
|
process.exit(1);
|
|
8166
8676
|
}
|
|
8167
8677
|
});
|
|
8168
8678
|
program2.command("daemon").description("Start the scheduler daemon").option("--interval <seconds>", "Check interval in seconds", "60").action(async (opts) => {
|
|
8169
8679
|
try {
|
|
8170
8680
|
const intervalMs = parseInt(opts.interval, 10) * 1000;
|
|
8171
|
-
log(
|
|
8172
|
-
log(
|
|
8681
|
+
log(chalk6.blue("Scheduler daemon started. Press Ctrl+C to stop."));
|
|
8682
|
+
log(chalk6.dim(` Check interval: ${opts.interval}s`));
|
|
8173
8683
|
let running = true;
|
|
8174
8684
|
const checkAndRun = async () => {
|
|
8175
8685
|
while (running) {
|
|
@@ -8178,7 +8688,7 @@ program2.command("daemon").description("Start the scheduler daemon").option("--i
|
|
|
8178
8688
|
const now2 = new Date().toISOString();
|
|
8179
8689
|
for (const schedule of schedules) {
|
|
8180
8690
|
if (schedule.nextRunAt && schedule.nextRunAt <= now2) {
|
|
8181
|
-
log(
|
|
8691
|
+
log(chalk6.blue(`[${new Date().toISOString()}] Triggering schedule: ${schedule.name}`));
|
|
8182
8692
|
try {
|
|
8183
8693
|
const { run } = await runByFilter({
|
|
8184
8694
|
url: schedule.url,
|
|
@@ -8191,35 +8701,35 @@ program2.command("daemon").description("Start the scheduler daemon").option("--i
|
|
|
8191
8701
|
timeout: schedule.timeoutMs ?? undefined,
|
|
8192
8702
|
projectId: schedule.projectId ?? undefined
|
|
8193
8703
|
});
|
|
8194
|
-
const statusColor = run.status === "passed" ?
|
|
8704
|
+
const statusColor = run.status === "passed" ? chalk6.green : chalk6.red;
|
|
8195
8705
|
log(` ${statusColor(run.status)} \u2014 ${run.passed}/${run.total} passed`);
|
|
8196
8706
|
updateSchedule(schedule.id, {});
|
|
8197
8707
|
} catch (err) {
|
|
8198
|
-
logError(
|
|
8708
|
+
logError(chalk6.red(` Error running schedule ${schedule.name}: ${err instanceof Error ? err.message : String(err)}`));
|
|
8199
8709
|
}
|
|
8200
8710
|
}
|
|
8201
8711
|
}
|
|
8202
8712
|
} catch (err) {
|
|
8203
|
-
logError(
|
|
8713
|
+
logError(chalk6.red(`Daemon error: ${err instanceof Error ? err.message : String(err)}`));
|
|
8204
8714
|
}
|
|
8205
8715
|
await new Promise((resolve2) => setTimeout(resolve2, intervalMs));
|
|
8206
8716
|
}
|
|
8207
8717
|
};
|
|
8208
8718
|
process.on("SIGINT", () => {
|
|
8209
|
-
log(
|
|
8719
|
+
log(chalk6.yellow(`
|
|
8210
8720
|
Shutting down scheduler daemon...`));
|
|
8211
8721
|
running = false;
|
|
8212
8722
|
process.exit(0);
|
|
8213
8723
|
});
|
|
8214
8724
|
process.on("SIGTERM", () => {
|
|
8215
|
-
log(
|
|
8725
|
+
log(chalk6.yellow(`
|
|
8216
8726
|
Shutting down scheduler daemon...`));
|
|
8217
8727
|
running = false;
|
|
8218
8728
|
process.exit(0);
|
|
8219
8729
|
});
|
|
8220
8730
|
await checkAndRun();
|
|
8221
8731
|
} catch (error) {
|
|
8222
|
-
logError(
|
|
8732
|
+
logError(chalk6.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
8223
8733
|
process.exit(1);
|
|
8224
8734
|
}
|
|
8225
8735
|
});
|
|
@@ -8231,21 +8741,21 @@ program2.command("init").description("Initialize a new testing project").option(
|
|
|
8231
8741
|
path: opts.path
|
|
8232
8742
|
});
|
|
8233
8743
|
log("");
|
|
8234
|
-
log(
|
|
8744
|
+
log(chalk6.bold(" Project initialized!"));
|
|
8235
8745
|
log("");
|
|
8236
8746
|
if (framework) {
|
|
8237
|
-
log(` Framework: ${
|
|
8747
|
+
log(` Framework: ${chalk6.cyan(framework.name)}`);
|
|
8238
8748
|
if (framework.features.length > 0) {
|
|
8239
|
-
log(` Features: ${
|
|
8749
|
+
log(` Features: ${chalk6.dim(framework.features.join(", "))}`);
|
|
8240
8750
|
}
|
|
8241
8751
|
} else {
|
|
8242
|
-
log(` Framework: ${
|
|
8752
|
+
log(` Framework: ${chalk6.dim("not detected")}`);
|
|
8243
8753
|
}
|
|
8244
|
-
log(` Project: ${
|
|
8245
|
-
log(` Scenarios: ${
|
|
8754
|
+
log(` Project: ${chalk6.green(project.name)} ${chalk6.dim(`(${project.id})`)}`);
|
|
8755
|
+
log(` Scenarios: ${chalk6.green(String(scenarios.length))} starter scenarios created`);
|
|
8246
8756
|
log("");
|
|
8247
8757
|
for (const s of scenarios) {
|
|
8248
|
-
log(` ${
|
|
8758
|
+
log(` ${chalk6.dim(s.shortId)} ${s.name} ${chalk6.dim(`[${s.tags.join(", ")}]`)}`);
|
|
8249
8759
|
}
|
|
8250
8760
|
if (opts.ci === "github") {
|
|
8251
8761
|
const workflowDir = join6(process.cwd(), ".github", "workflows");
|
|
@@ -8254,9 +8764,9 @@ program2.command("init").description("Initialize a new testing project").option(
|
|
|
8254
8764
|
}
|
|
8255
8765
|
const workflowPath = join6(workflowDir, "testers.yml");
|
|
8256
8766
|
writeFileSync3(workflowPath, generateGitHubActionsWorkflow(), "utf-8");
|
|
8257
|
-
log(` CI: ${
|
|
8767
|
+
log(` CI: ${chalk6.green("GitHub Actions workflow written to .github/workflows/testers.yml")}`);
|
|
8258
8768
|
} else if (opts.ci) {
|
|
8259
|
-
log(
|
|
8769
|
+
log(chalk6.yellow(` Unknown CI provider: ${opts.ci}. Supported: github`));
|
|
8260
8770
|
}
|
|
8261
8771
|
log("");
|
|
8262
8772
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
@@ -8269,7 +8779,7 @@ program2.command("init").description("Initialize a new testing project").option(
|
|
|
8269
8779
|
const resolvedEnvName = envName.trim() || "staging";
|
|
8270
8780
|
const resolvedEnvUrl = envUrl.trim() || url;
|
|
8271
8781
|
createEnvironment({ name: resolvedEnvName, url: resolvedEnvUrl, projectId: project.id, isDefault: true });
|
|
8272
|
-
log(
|
|
8782
|
+
log(chalk6.green(` \u2713 Environment '${resolvedEnvName}' created (${resolvedEnvUrl})`));
|
|
8273
8783
|
log("");
|
|
8274
8784
|
}
|
|
8275
8785
|
const scenarioAnswer = await ask(" Would you like to create your first test scenario? [y/N] ");
|
|
@@ -8286,19 +8796,19 @@ program2.command("init").description("Initialize a new testing project").option(
|
|
|
8286
8796
|
tags: ["smoke"],
|
|
8287
8797
|
priority: "high"
|
|
8288
8798
|
});
|
|
8289
|
-
log(
|
|
8799
|
+
log(chalk6.green(` \u2713 Scenario '${newScenario.name}' created ${chalk6.dim(`(${newScenario.shortId})`)}`));
|
|
8290
8800
|
log("");
|
|
8291
8801
|
}
|
|
8292
8802
|
} finally {
|
|
8293
8803
|
rl.close();
|
|
8294
8804
|
}
|
|
8295
|
-
log(
|
|
8805
|
+
log(chalk6.bold(" Next steps:"));
|
|
8296
8806
|
log(` 1. Start your dev server`);
|
|
8297
|
-
log(` 2. Run ${
|
|
8298
|
-
log(` 3. Add more scenarios with ${
|
|
8807
|
+
log(` 2. Run ${chalk6.cyan("testers run <url>")} to execute tests`);
|
|
8808
|
+
log(` 3. Add more scenarios with ${chalk6.cyan("testers add <name>")}`);
|
|
8299
8809
|
log("");
|
|
8300
8810
|
} catch (error) {
|
|
8301
|
-
logError(
|
|
8811
|
+
logError(chalk6.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
8302
8812
|
process.exit(1);
|
|
8303
8813
|
}
|
|
8304
8814
|
});
|
|
@@ -8306,16 +8816,16 @@ program2.command("replay <run-id>").description("Re-run all scenarios from a pre
|
|
|
8306
8816
|
try {
|
|
8307
8817
|
const originalRun = getRun(runId);
|
|
8308
8818
|
if (!originalRun) {
|
|
8309
|
-
logError(
|
|
8819
|
+
logError(chalk6.red(`Run not found: ${runId}`));
|
|
8310
8820
|
process.exit(1);
|
|
8311
8821
|
}
|
|
8312
8822
|
const originalResults = getResultsByRun(originalRun.id);
|
|
8313
8823
|
const scenarioIds = originalResults.map((r) => r.scenarioId);
|
|
8314
8824
|
if (scenarioIds.length === 0) {
|
|
8315
|
-
log(
|
|
8825
|
+
log(chalk6.dim("No scenarios to replay."));
|
|
8316
8826
|
return;
|
|
8317
8827
|
}
|
|
8318
|
-
log(
|
|
8828
|
+
log(chalk6.blue(`Replaying ${scenarioIds.length} scenarios from run ${originalRun.id.slice(0, 8)}...`));
|
|
8319
8829
|
const { run, results } = await runByFilter({
|
|
8320
8830
|
url: opts.url ?? originalRun.url,
|
|
8321
8831
|
scenarioIds,
|
|
@@ -8330,7 +8840,7 @@ program2.command("replay <run-id>").description("Re-run all scenarios from a pre
|
|
|
8330
8840
|
}
|
|
8331
8841
|
process.exit(getExitCode(run));
|
|
8332
8842
|
} catch (error) {
|
|
8333
|
-
logError(
|
|
8843
|
+
logError(chalk6.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
8334
8844
|
process.exit(1);
|
|
8335
8845
|
}
|
|
8336
8846
|
});
|
|
@@ -8338,16 +8848,16 @@ program2.command("retry <run-id>").description("Re-run only failed scenarios fro
|
|
|
8338
8848
|
try {
|
|
8339
8849
|
const originalRun = getRun(runId);
|
|
8340
8850
|
if (!originalRun) {
|
|
8341
|
-
logError(
|
|
8851
|
+
logError(chalk6.red(`Run not found: ${runId}`));
|
|
8342
8852
|
process.exit(1);
|
|
8343
8853
|
}
|
|
8344
8854
|
const originalResults = getResultsByRun(originalRun.id);
|
|
8345
8855
|
const failedScenarioIds = originalResults.filter((r) => r.status === "failed" || r.status === "error").map((r) => r.scenarioId);
|
|
8346
8856
|
if (failedScenarioIds.length === 0) {
|
|
8347
|
-
log(
|
|
8857
|
+
log(chalk6.green("No failed scenarios to retry. All passed!"));
|
|
8348
8858
|
return;
|
|
8349
8859
|
}
|
|
8350
|
-
log(
|
|
8860
|
+
log(chalk6.blue(`Retrying ${failedScenarioIds.length} failed scenarios from run ${originalRun.id.slice(0, 8)}...`));
|
|
8351
8861
|
const { run, results } = await runByFilter({
|
|
8352
8862
|
url: opts.url ?? originalRun.url,
|
|
8353
8863
|
scenarioIds: failedScenarioIds,
|
|
@@ -8357,13 +8867,13 @@ program2.command("retry <run-id>").description("Re-run only failed scenarios fro
|
|
|
8357
8867
|
});
|
|
8358
8868
|
if (!opts.json) {
|
|
8359
8869
|
log("");
|
|
8360
|
-
log(
|
|
8870
|
+
log(chalk6.bold(" Comparison with original run:"));
|
|
8361
8871
|
for (const result of results) {
|
|
8362
8872
|
const original = originalResults.find((r) => r.scenarioId === result.scenarioId);
|
|
8363
8873
|
if (original) {
|
|
8364
8874
|
const changed = original.status !== result.status;
|
|
8365
|
-
const arrow = changed ?
|
|
8366
|
-
const icon = result.status === "passed" ?
|
|
8875
|
+
const arrow = changed ? chalk6.yellow(`${original.status} \u2192 ${result.status}`) : chalk6.dim(`${result.status} (unchanged)`);
|
|
8876
|
+
const icon = result.status === "passed" ? chalk6.green("\u2713") : chalk6.red("\u2717");
|
|
8367
8877
|
log(` ${icon} ${result.scenarioId.slice(0, 8)}: ${arrow}`);
|
|
8368
8878
|
}
|
|
8369
8879
|
}
|
|
@@ -8376,14 +8886,14 @@ program2.command("retry <run-id>").description("Re-run only failed scenarios fro
|
|
|
8376
8886
|
}
|
|
8377
8887
|
process.exit(getExitCode(run));
|
|
8378
8888
|
} catch (error) {
|
|
8379
|
-
logError(
|
|
8889
|
+
logError(chalk6.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
8380
8890
|
process.exit(1);
|
|
8381
8891
|
}
|
|
8382
8892
|
});
|
|
8383
8893
|
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) => {
|
|
8384
8894
|
try {
|
|
8385
8895
|
const projectId = resolveProject(opts.project);
|
|
8386
|
-
log(
|
|
8896
|
+
log(chalk6.blue(`Running smoke test against ${chalk6.bold(url)}...`));
|
|
8387
8897
|
log("");
|
|
8388
8898
|
const smokeResult = await runSmoke({
|
|
8389
8899
|
url,
|
|
@@ -8405,7 +8915,7 @@ program2.command("smoke <url>").description("Run autonomous smoke test").option(
|
|
|
8405
8915
|
const hasCritical = smokeResult.issuesFound.some((i) => i.severity === "critical" || i.severity === "high");
|
|
8406
8916
|
process.exit(hasCritical ? 1 : 0);
|
|
8407
8917
|
} catch (error) {
|
|
8408
|
-
logError(
|
|
8918
|
+
logError(chalk6.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
8409
8919
|
process.exit(1);
|
|
8410
8920
|
}
|
|
8411
8921
|
});
|
|
@@ -8429,7 +8939,7 @@ program2.command("diff <run1> <run2>").description("Compare two test runs").opti
|
|
|
8429
8939
|
const hasVisualRegressions = visualResults.some((r) => r.isRegression);
|
|
8430
8940
|
process.exit(diff.regressions.length > 0 || hasVisualRegressions ? 1 : 0);
|
|
8431
8941
|
} catch (error) {
|
|
8432
|
-
logError(
|
|
8942
|
+
logError(chalk6.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
8433
8943
|
process.exit(1);
|
|
8434
8944
|
}
|
|
8435
8945
|
});
|
|
@@ -8443,13 +8953,13 @@ program2.command("report [run-id]").description("Generate HTML test report").opt
|
|
|
8443
8953
|
}
|
|
8444
8954
|
writeFileSync3(opts.output, html, "utf-8");
|
|
8445
8955
|
const absPath = resolve(opts.output);
|
|
8446
|
-
log(
|
|
8956
|
+
log(chalk6.green(`Report generated: ${absPath}`));
|
|
8447
8957
|
if (opts.open) {
|
|
8448
8958
|
const openCmd = process.platform === "darwin" ? "open" : "xdg-open";
|
|
8449
8959
|
Bun.spawn([openCmd, absPath]);
|
|
8450
8960
|
}
|
|
8451
8961
|
} catch (error) {
|
|
8452
|
-
logError(
|
|
8962
|
+
logError(chalk6.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
8453
8963
|
process.exit(1);
|
|
8454
8964
|
}
|
|
8455
8965
|
});
|
|
@@ -8462,9 +8972,9 @@ authCmd.command("add <name>").description("Create an auth preset").requiredOptio
|
|
|
8462
8972
|
password: opts.password,
|
|
8463
8973
|
loginPath: opts.loginPath
|
|
8464
8974
|
});
|
|
8465
|
-
log(
|
|
8975
|
+
log(chalk6.green(`Created auth preset ${chalk6.bold(preset.name)} (${preset.email})`));
|
|
8466
8976
|
} catch (error) {
|
|
8467
|
-
logError(
|
|
8977
|
+
logError(chalk6.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
8468
8978
|
process.exit(1);
|
|
8469
8979
|
}
|
|
8470
8980
|
});
|
|
@@ -8472,11 +8982,11 @@ authCmd.command("list").description("List auth presets").action(() => {
|
|
|
8472
8982
|
try {
|
|
8473
8983
|
const presets = listAuthPresets();
|
|
8474
8984
|
if (presets.length === 0) {
|
|
8475
|
-
log(
|
|
8985
|
+
log(chalk6.dim("No auth presets found."));
|
|
8476
8986
|
return;
|
|
8477
8987
|
}
|
|
8478
8988
|
log("");
|
|
8479
|
-
log(
|
|
8989
|
+
log(chalk6.bold(" Auth Presets"));
|
|
8480
8990
|
log("");
|
|
8481
8991
|
log(` ${"Name".padEnd(20)} ${"Email".padEnd(30)} ${"Login Path".padEnd(15)} Created`);
|
|
8482
8992
|
log(` ${"\u2500".repeat(20)} ${"\u2500".repeat(30)} ${"\u2500".repeat(15)} ${"\u2500".repeat(22)}`);
|
|
@@ -8485,7 +8995,7 @@ authCmd.command("list").description("List auth presets").action(() => {
|
|
|
8485
8995
|
}
|
|
8486
8996
|
log("");
|
|
8487
8997
|
} catch (error) {
|
|
8488
|
-
logError(
|
|
8998
|
+
logError(chalk6.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
8489
8999
|
process.exit(1);
|
|
8490
9000
|
}
|
|
8491
9001
|
});
|
|
@@ -8493,13 +9003,13 @@ authCmd.command("delete <name>").description("Delete an auth preset").action((na
|
|
|
8493
9003
|
try {
|
|
8494
9004
|
const deleted = deleteAuthPreset(name);
|
|
8495
9005
|
if (deleted) {
|
|
8496
|
-
log(
|
|
9006
|
+
log(chalk6.green(`Deleted auth preset: ${name}`));
|
|
8497
9007
|
} else {
|
|
8498
|
-
logError(
|
|
9008
|
+
logError(chalk6.red(`Auth preset not found: ${name}`));
|
|
8499
9009
|
process.exit(1);
|
|
8500
9010
|
}
|
|
8501
9011
|
} catch (error) {
|
|
8502
|
-
logError(
|
|
9012
|
+
logError(chalk6.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
8503
9013
|
process.exit(1);
|
|
8504
9014
|
}
|
|
8505
9015
|
});
|
|
@@ -8525,7 +9035,7 @@ program2.command("costs").description("Show cost tracking and budget status").op
|
|
|
8525
9035
|
log(formatCostsTerminal(summary));
|
|
8526
9036
|
}
|
|
8527
9037
|
} catch (error) {
|
|
8528
|
-
logError(
|
|
9038
|
+
logError(chalk6.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
8529
9039
|
process.exit(1);
|
|
8530
9040
|
}
|
|
8531
9041
|
});
|
|
@@ -8533,18 +9043,18 @@ program2.command("chain <scenario-id>").description("Add a dependency to a scena
|
|
|
8533
9043
|
try {
|
|
8534
9044
|
const scenario = getScenario(scenarioId) ?? getScenarioByShortId(scenarioId);
|
|
8535
9045
|
if (!scenario) {
|
|
8536
|
-
logError(
|
|
9046
|
+
logError(chalk6.red(`Scenario not found: ${scenarioId}`));
|
|
8537
9047
|
process.exit(1);
|
|
8538
9048
|
}
|
|
8539
9049
|
const dep = getScenario(opts.dependsOn) ?? getScenarioByShortId(opts.dependsOn);
|
|
8540
9050
|
if (!dep) {
|
|
8541
|
-
logError(
|
|
9051
|
+
logError(chalk6.red(`Dependency scenario not found: ${opts.dependsOn}`));
|
|
8542
9052
|
process.exit(1);
|
|
8543
9053
|
}
|
|
8544
9054
|
addDependency(scenario.id, dep.id);
|
|
8545
|
-
log(
|
|
9055
|
+
log(chalk6.green(`${scenario.shortId} now depends on ${dep.shortId}`));
|
|
8546
9056
|
} catch (error) {
|
|
8547
|
-
logError(
|
|
9057
|
+
logError(chalk6.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
8548
9058
|
process.exit(1);
|
|
8549
9059
|
}
|
|
8550
9060
|
});
|
|
@@ -8552,18 +9062,18 @@ program2.command("unchain <scenario-id>").description("Remove a dependency from
|
|
|
8552
9062
|
try {
|
|
8553
9063
|
const scenario = getScenario(scenarioId) ?? getScenarioByShortId(scenarioId);
|
|
8554
9064
|
if (!scenario) {
|
|
8555
|
-
logError(
|
|
9065
|
+
logError(chalk6.red(`Scenario not found: ${scenarioId}`));
|
|
8556
9066
|
process.exit(1);
|
|
8557
9067
|
}
|
|
8558
9068
|
const dep = getScenario(opts.from) ?? getScenarioByShortId(opts.from);
|
|
8559
9069
|
if (!dep) {
|
|
8560
|
-
logError(
|
|
9070
|
+
logError(chalk6.red(`Dependency not found: ${opts.from}`));
|
|
8561
9071
|
process.exit(1);
|
|
8562
9072
|
}
|
|
8563
9073
|
removeDependency(scenario.id, dep.id);
|
|
8564
|
-
log(
|
|
9074
|
+
log(chalk6.green(`Removed dependency: ${scenario.shortId} no longer depends on ${dep.shortId}`));
|
|
8565
9075
|
} catch (error) {
|
|
8566
|
-
logError(
|
|
9076
|
+
logError(chalk6.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
8567
9077
|
process.exit(1);
|
|
8568
9078
|
}
|
|
8569
9079
|
});
|
|
@@ -8571,26 +9081,26 @@ program2.command("deps <scenario-id>").description("Show dependencies for a scen
|
|
|
8571
9081
|
try {
|
|
8572
9082
|
const scenario = getScenario(scenarioId) ?? getScenarioByShortId(scenarioId);
|
|
8573
9083
|
if (!scenario) {
|
|
8574
|
-
logError(
|
|
9084
|
+
logError(chalk6.red(`Scenario not found: ${scenarioId}`));
|
|
8575
9085
|
process.exit(1);
|
|
8576
9086
|
}
|
|
8577
9087
|
const deps = getDependencies(scenario.id);
|
|
8578
9088
|
const dependents = getDependents(scenario.id);
|
|
8579
9089
|
log("");
|
|
8580
|
-
log(
|
|
9090
|
+
log(chalk6.bold(` Dependencies for ${scenario.shortId}: ${scenario.name}`));
|
|
8581
9091
|
log("");
|
|
8582
9092
|
if (deps.length > 0) {
|
|
8583
|
-
log(
|
|
9093
|
+
log(chalk6.dim(" Depends on:"));
|
|
8584
9094
|
for (const depId of deps) {
|
|
8585
9095
|
const s = getScenario(depId);
|
|
8586
9096
|
log(` \u2192 ${s ? `${s.shortId}: ${s.name}` : depId.slice(0, 8)}`);
|
|
8587
9097
|
}
|
|
8588
9098
|
} else {
|
|
8589
|
-
log(
|
|
9099
|
+
log(chalk6.dim(" No dependencies"));
|
|
8590
9100
|
}
|
|
8591
9101
|
if (dependents.length > 0) {
|
|
8592
9102
|
log("");
|
|
8593
|
-
log(
|
|
9103
|
+
log(chalk6.dim(" Required by:"));
|
|
8594
9104
|
for (const depId of dependents) {
|
|
8595
9105
|
const s = getScenario(depId);
|
|
8596
9106
|
log(` \u2190 ${s ? `${s.shortId}: ${s.name}` : depId.slice(0, 8)}`);
|
|
@@ -8598,7 +9108,7 @@ program2.command("deps <scenario-id>").description("Show dependencies for a scen
|
|
|
8598
9108
|
}
|
|
8599
9109
|
log("");
|
|
8600
9110
|
} catch (error) {
|
|
8601
|
-
logError(
|
|
9111
|
+
logError(chalk6.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
8602
9112
|
process.exit(1);
|
|
8603
9113
|
}
|
|
8604
9114
|
});
|
|
@@ -8608,7 +9118,7 @@ flowCmd.command("create <name>").description("Create a flow from scenario IDs").
|
|
|
8608
9118
|
const ids = opts.chain.split(",").map((id) => {
|
|
8609
9119
|
const s = getScenario(id.trim()) ?? getScenarioByShortId(id.trim());
|
|
8610
9120
|
if (!s) {
|
|
8611
|
-
logError(
|
|
9121
|
+
logError(chalk6.red(`Scenario not found: ${id.trim()}`));
|
|
8612
9122
|
process.exit(1);
|
|
8613
9123
|
}
|
|
8614
9124
|
return s.id;
|
|
@@ -8619,37 +9129,37 @@ flowCmd.command("create <name>").description("Create a flow from scenario IDs").
|
|
|
8619
9129
|
} catch {}
|
|
8620
9130
|
}
|
|
8621
9131
|
const flow = createFlow({ name, scenarioIds: ids, projectId: resolveProject(opts.project) });
|
|
8622
|
-
log(
|
|
9132
|
+
log(chalk6.green(`Flow created: ${flow.id.slice(0, 8)} \u2014 ${flow.name} (${ids.length} scenarios)`));
|
|
8623
9133
|
} catch (error) {
|
|
8624
|
-
logError(
|
|
9134
|
+
logError(chalk6.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
8625
9135
|
process.exit(1);
|
|
8626
9136
|
}
|
|
8627
9137
|
});
|
|
8628
9138
|
flowCmd.command("list").description("List all flows").option("--project <id>", "Project ID").action((opts) => {
|
|
8629
9139
|
const flows = listFlows(resolveProject(opts.project) ?? undefined);
|
|
8630
9140
|
if (flows.length === 0) {
|
|
8631
|
-
log(
|
|
9141
|
+
log(chalk6.dim(`
|
|
8632
9142
|
No flows found.
|
|
8633
9143
|
`));
|
|
8634
9144
|
return;
|
|
8635
9145
|
}
|
|
8636
9146
|
log("");
|
|
8637
|
-
log(
|
|
9147
|
+
log(chalk6.bold(" Flows"));
|
|
8638
9148
|
log("");
|
|
8639
9149
|
for (const f of flows) {
|
|
8640
|
-
log(` ${
|
|
9150
|
+
log(` ${chalk6.dim(f.id.slice(0, 8))} ${f.name} ${chalk6.dim(`(${f.scenarioIds.length} scenarios)`)}`);
|
|
8641
9151
|
}
|
|
8642
9152
|
log("");
|
|
8643
9153
|
});
|
|
8644
9154
|
flowCmd.command("show <id>").description("Show flow details").action((id) => {
|
|
8645
9155
|
const flow = getFlow(id);
|
|
8646
9156
|
if (!flow) {
|
|
8647
|
-
logError(
|
|
9157
|
+
logError(chalk6.red(`Flow not found: ${id}`));
|
|
8648
9158
|
process.exit(1);
|
|
8649
9159
|
}
|
|
8650
9160
|
log("");
|
|
8651
|
-
log(
|
|
8652
|
-
log(` ID: ${
|
|
9161
|
+
log(chalk6.bold(` Flow: ${flow.name}`));
|
|
9162
|
+
log(` ID: ${chalk6.dim(flow.id)}`);
|
|
8653
9163
|
log(` Scenarios (in order):`);
|
|
8654
9164
|
for (let i = 0;i < flow.scenarioIds.length; i++) {
|
|
8655
9165
|
const s = getScenario(flow.scenarioIds[i]);
|
|
@@ -8659,9 +9169,9 @@ flowCmd.command("show <id>").description("Show flow details").action((id) => {
|
|
|
8659
9169
|
});
|
|
8660
9170
|
flowCmd.command("delete <id>").description("Delete a flow").action((id) => {
|
|
8661
9171
|
if (deleteFlow(id))
|
|
8662
|
-
log(
|
|
9172
|
+
log(chalk6.green("Flow deleted."));
|
|
8663
9173
|
else {
|
|
8664
|
-
logError(
|
|
9174
|
+
logError(chalk6.red("Flow not found."));
|
|
8665
9175
|
process.exit(1);
|
|
8666
9176
|
}
|
|
8667
9177
|
});
|
|
@@ -8669,14 +9179,14 @@ flowCmd.command("run <id>").description("Run a flow (scenarios in dependency ord
|
|
|
8669
9179
|
try {
|
|
8670
9180
|
const flow = getFlow(id);
|
|
8671
9181
|
if (!flow) {
|
|
8672
|
-
logError(
|
|
9182
|
+
logError(chalk6.red(`Flow not found: ${id}`));
|
|
8673
9183
|
process.exit(1);
|
|
8674
9184
|
}
|
|
8675
9185
|
if (!opts.url) {
|
|
8676
|
-
logError(
|
|
9186
|
+
logError(chalk6.red("--url is required for flow run"));
|
|
8677
9187
|
process.exit(1);
|
|
8678
9188
|
}
|
|
8679
|
-
log(
|
|
9189
|
+
log(chalk6.blue(`Running flow: ${flow.name} (${flow.scenarioIds.length} scenarios)`));
|
|
8680
9190
|
const { run, results } = await runByFilter({
|
|
8681
9191
|
url: opts.url,
|
|
8682
9192
|
scenarioIds: flow.scenarioIds,
|
|
@@ -8690,7 +9200,7 @@ flowCmd.command("run <id>").description("Run a flow (scenarios in dependency ord
|
|
|
8690
9200
|
log(formatTerminal(run, results));
|
|
8691
9201
|
process.exit(getExitCode(run));
|
|
8692
9202
|
} catch (error) {
|
|
8693
|
-
logError(
|
|
9203
|
+
logError(chalk6.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
8694
9204
|
process.exit(1);
|
|
8695
9205
|
}
|
|
8696
9206
|
});
|
|
@@ -8704,9 +9214,9 @@ envCmd.command("add <name>").description("Add a named environment").requiredOpti
|
|
|
8704
9214
|
projectId: opts.project,
|
|
8705
9215
|
isDefault: opts.default
|
|
8706
9216
|
});
|
|
8707
|
-
log(
|
|
9217
|
+
log(chalk6.green(`Environment added: ${env.name} \u2192 ${env.url}${env.isDefault ? " (default)" : ""}`));
|
|
8708
9218
|
} catch (error) {
|
|
8709
|
-
logError(
|
|
9219
|
+
logError(chalk6.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
8710
9220
|
process.exit(1);
|
|
8711
9221
|
}
|
|
8712
9222
|
});
|
|
@@ -8714,16 +9224,16 @@ envCmd.command("list").description("List all environments").option("--project <i
|
|
|
8714
9224
|
try {
|
|
8715
9225
|
const envs = listEnvironments(opts.project);
|
|
8716
9226
|
if (envs.length === 0) {
|
|
8717
|
-
log(
|
|
9227
|
+
log(chalk6.dim("No environments configured. Add one with: testers env add <name> --url <url>"));
|
|
8718
9228
|
return;
|
|
8719
9229
|
}
|
|
8720
9230
|
for (const env of envs) {
|
|
8721
|
-
const marker = env.isDefault ?
|
|
8722
|
-
const auth = env.authPresetName ?
|
|
8723
|
-
log(` ${
|
|
9231
|
+
const marker = env.isDefault ? chalk6.green(" \u2605 default") : "";
|
|
9232
|
+
const auth = env.authPresetName ? chalk6.dim(` (auth: ${env.authPresetName})`) : "";
|
|
9233
|
+
log(` ${chalk6.bold(env.name)} ${env.url}${auth}${marker}`);
|
|
8724
9234
|
}
|
|
8725
9235
|
} catch (error) {
|
|
8726
|
-
logError(
|
|
9236
|
+
logError(chalk6.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
8727
9237
|
process.exit(1);
|
|
8728
9238
|
}
|
|
8729
9239
|
});
|
|
@@ -8731,9 +9241,9 @@ envCmd.command("use <name>").description("Set an environment as the default").ac
|
|
|
8731
9241
|
try {
|
|
8732
9242
|
setDefaultEnvironment(name);
|
|
8733
9243
|
const env = getEnvironment(name);
|
|
8734
|
-
log(
|
|
9244
|
+
log(chalk6.green(`Default environment set: ${env.name} \u2192 ${env.url}`));
|
|
8735
9245
|
} catch (error) {
|
|
8736
|
-
logError(
|
|
9246
|
+
logError(chalk6.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
8737
9247
|
process.exit(1);
|
|
8738
9248
|
}
|
|
8739
9249
|
});
|
|
@@ -8741,13 +9251,13 @@ envCmd.command("delete <name>").description("Delete an environment").action((nam
|
|
|
8741
9251
|
try {
|
|
8742
9252
|
const deleted = deleteEnvironment(name);
|
|
8743
9253
|
if (deleted) {
|
|
8744
|
-
log(
|
|
9254
|
+
log(chalk6.green(`Environment deleted: ${name}`));
|
|
8745
9255
|
} else {
|
|
8746
|
-
logError(
|
|
9256
|
+
logError(chalk6.red(`Environment not found: ${name}`));
|
|
8747
9257
|
process.exit(1);
|
|
8748
9258
|
}
|
|
8749
9259
|
} catch (error) {
|
|
8750
|
-
logError(
|
|
9260
|
+
logError(chalk6.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
8751
9261
|
process.exit(1);
|
|
8752
9262
|
}
|
|
8753
9263
|
});
|
|
@@ -8755,9 +9265,9 @@ program2.command("baseline <run-id>").description("Set a run as the visual basel
|
|
|
8755
9265
|
try {
|
|
8756
9266
|
setBaseline(runId);
|
|
8757
9267
|
const run = getRun(runId);
|
|
8758
|
-
log(
|
|
9268
|
+
log(chalk6.green(`Baseline set: ${chalk6.bold(runId.slice(0, 8))}${run ? ` (${run.status}, ${run.total} scenarios)` : ""}`));
|
|
8759
9269
|
} catch (error) {
|
|
8760
|
-
logError(
|
|
9270
|
+
logError(chalk6.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
8761
9271
|
process.exit(1);
|
|
8762
9272
|
}
|
|
8763
9273
|
});
|
|
@@ -8765,28 +9275,159 @@ program2.command("import-api <spec>").description("Import test scenarios from an
|
|
|
8765
9275
|
try {
|
|
8766
9276
|
const { importFromOpenAPI: importFromOpenAPI2 } = await Promise.resolve().then(() => (init_openapi_import(), exports_openapi_import));
|
|
8767
9277
|
const { imported, scenarios } = importFromOpenAPI2(spec, resolveProject(opts.project) ?? undefined);
|
|
8768
|
-
log(
|
|
9278
|
+
log(chalk6.green(`
|
|
8769
9279
|
Imported ${imported} scenarios from API spec:`));
|
|
8770
9280
|
for (const s of scenarios) {
|
|
8771
|
-
log(` ${
|
|
9281
|
+
log(` ${chalk6.cyan(s.shortId)} ${s.name} ${chalk6.dim(`[${s.tags.join(", ")}]`)}`);
|
|
8772
9282
|
}
|
|
8773
9283
|
log("");
|
|
8774
9284
|
} catch (error) {
|
|
8775
|
-
logError(
|
|
9285
|
+
logError(chalk6.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
8776
9286
|
process.exit(1);
|
|
8777
9287
|
}
|
|
8778
9288
|
});
|
|
8779
9289
|
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) => {
|
|
8780
9290
|
try {
|
|
8781
9291
|
const { recordAndSave: recordAndSave2 } = await Promise.resolve().then(() => (init_recorder(), exports_recorder));
|
|
8782
|
-
log(
|
|
9292
|
+
log(chalk6.blue("Opening browser for recording..."));
|
|
8783
9293
|
const { recording, scenario } = await recordAndSave2(url, opts.name, resolveProject(opts.project) ?? undefined);
|
|
8784
9294
|
log("");
|
|
8785
|
-
log(
|
|
8786
|
-
log(
|
|
8787
|
-
log(
|
|
9295
|
+
log(chalk6.green(`Recording saved as scenario ${chalk6.bold(scenario.shortId)}: ${scenario.name}`));
|
|
9296
|
+
log(chalk6.dim(` ${recording.actions.length} actions recorded in ${(recording.duration / 1000).toFixed(0)}s`));
|
|
9297
|
+
log(chalk6.dim(` ${scenario.steps.length} steps generated`));
|
|
9298
|
+
} catch (error) {
|
|
9299
|
+
logError(chalk6.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
9300
|
+
process.exit(1);
|
|
9301
|
+
}
|
|
9302
|
+
});
|
|
9303
|
+
program2.command("run-affected <url>").description("Run only scenarios relevant to changed files (diff-aware testing)").option("-f, --file <path>", "Changed file path (repeatable)", (v, acc) => {
|
|
9304
|
+
acc.push(v);
|
|
9305
|
+
return acc;
|
|
9306
|
+
}, []).option("--map <glob:tags>", "Glob\u2192tag mapping, e.g. 'src/chat*:chat,messaging' (repeatable)", (v, acc) => {
|
|
9307
|
+
acc.push(v);
|
|
9308
|
+
return acc;
|
|
9309
|
+
}, []).option("--project <id>", "Project ID").option("-m, --model <model>", "AI model to use").option("--headed", "Run browser in headed mode", false).option("--parallel <n>", "Number of parallel browsers", "1").option("--json", "Output results as JSON", false).action(async (url, opts) => {
|
|
9310
|
+
try {
|
|
9311
|
+
const { matchFilesToScenarios: matchFilesToScenarios2 } = await Promise.resolve().then(() => exports_affected);
|
|
9312
|
+
const { runBatch: runBatch2 } = await Promise.resolve().then(() => (init_runner(), exports_runner));
|
|
9313
|
+
const projectId = resolveProject(opts.project);
|
|
9314
|
+
const mappings = opts.map.map((m) => {
|
|
9315
|
+
const sep = m.lastIndexOf(":");
|
|
9316
|
+
if (sep < 1)
|
|
9317
|
+
return null;
|
|
9318
|
+
return { glob: m.slice(0, sep), tags: m.slice(sep + 1).split(",").map((t) => t.trim()) };
|
|
9319
|
+
}).filter(Boolean);
|
|
9320
|
+
const allScenarios = listScenarios({ projectId });
|
|
9321
|
+
const matched = matchFilesToScenarios2(opts.file, allScenarios, mappings);
|
|
9322
|
+
if (matched.length === 0) {
|
|
9323
|
+
log(chalk6.yellow(" No scenarios matched the provided file paths."));
|
|
9324
|
+
log(chalk6.dim(" Tip: use --map 'src/chat*:chat' to add explicit mappings."));
|
|
9325
|
+
process.exit(0);
|
|
9326
|
+
}
|
|
9327
|
+
log(chalk6.blue(` Running ${matched.length} affected scenario(s) against ${url}...`));
|
|
9328
|
+
log(chalk6.dim(` Files: ${opts.file.slice(0, 5).join(", ")}${opts.file.length > 5 ? "\u2026" : ""}`));
|
|
9329
|
+
log("");
|
|
9330
|
+
const { run, results } = await runBatch2(matched, {
|
|
9331
|
+
url,
|
|
9332
|
+
model: opts.model,
|
|
9333
|
+
headed: opts.headed,
|
|
9334
|
+
parallel: parseInt(opts.parallel, 10),
|
|
9335
|
+
projectId
|
|
9336
|
+
});
|
|
9337
|
+
if (opts.json) {
|
|
9338
|
+
log(JSON.stringify({ run, results }, null, 2));
|
|
9339
|
+
} else {
|
|
9340
|
+
const { formatTerminal: formatTerminal2 } = await Promise.resolve().then(() => (init_reporter(), exports_reporter));
|
|
9341
|
+
log(formatTerminal2(run, results));
|
|
9342
|
+
}
|
|
9343
|
+
const { getExitCode: getExitCode2 } = await Promise.resolve().then(() => (init_reporter(), exports_reporter));
|
|
9344
|
+
process.exit(getExitCode2(run));
|
|
9345
|
+
} catch (error) {
|
|
9346
|
+
logError(chalk6.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
9347
|
+
process.exit(1);
|
|
9348
|
+
}
|
|
9349
|
+
});
|
|
9350
|
+
program2.command("git-watch <url>").description("Watch for git commits and auto-run affected scenarios").option("--dir <path>", "Git repository directory to watch", process.cwd()).option("--poll <ms>", "Poll interval in milliseconds", "10000").option("--map <glob:tags>", "Glob\u2192tag mapping (repeatable)", (v, acc) => {
|
|
9351
|
+
acc.push(v);
|
|
9352
|
+
return acc;
|
|
9353
|
+
}, []).option("--project <id>", "Project ID").option("-m, --model <model>", "AI model to use").option("--headed", "Run browser in headed mode", false).option("--parallel <n>", "Number of parallel browsers", "1").action(async (url, opts) => {
|
|
9354
|
+
try {
|
|
9355
|
+
const { startGitWatcher: startGitWatcher2 } = await Promise.resolve().then(() => (init_git_watch(), exports_git_watch));
|
|
9356
|
+
const mappings = opts.map.map((m) => {
|
|
9357
|
+
const sep = m.lastIndexOf(":");
|
|
9358
|
+
if (sep < 1)
|
|
9359
|
+
return null;
|
|
9360
|
+
return { glob: m.slice(0, sep), tags: m.slice(sep + 1).split(",").map((t) => t.trim()) };
|
|
9361
|
+
}).filter(Boolean);
|
|
9362
|
+
await startGitWatcher2({
|
|
9363
|
+
url,
|
|
9364
|
+
dir: opts.dir,
|
|
9365
|
+
pollIntervalMs: parseInt(opts.poll, 10),
|
|
9366
|
+
mappings,
|
|
9367
|
+
projectId: resolveProject(opts.project),
|
|
9368
|
+
model: opts.model,
|
|
9369
|
+
headed: opts.headed,
|
|
9370
|
+
parallel: parseInt(opts.parallel, 10)
|
|
9371
|
+
});
|
|
9372
|
+
} catch (error) {
|
|
9373
|
+
logError(chalk6.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
9374
|
+
process.exit(1);
|
|
9375
|
+
}
|
|
9376
|
+
});
|
|
9377
|
+
var agentCmd = program2.command("agent").description("Manage registered agents");
|
|
9378
|
+
agentCmd.command("register <name>").description("Register an agent (idempotent)").option("-d, --description <text>", "Agent description").option("-r, --role <role>", "Agent role").action((name, opts) => {
|
|
9379
|
+
try {
|
|
9380
|
+
const { registerAgent: registerAgent2 } = (init_agents(), __toCommonJS(exports_agents));
|
|
9381
|
+
const agent = registerAgent2({ name, description: opts.description, role: opts.role });
|
|
9382
|
+
log(chalk6.green(`Registered agent: ${agent.name} (${agent.id.slice(0, 8)})`));
|
|
9383
|
+
} catch (error) {
|
|
9384
|
+
logError(chalk6.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
9385
|
+
process.exit(1);
|
|
9386
|
+
}
|
|
9387
|
+
});
|
|
9388
|
+
agentCmd.command("heartbeat <id>").description("Update agent last_seen_at timestamp").action((id) => {
|
|
9389
|
+
try {
|
|
9390
|
+
const { heartbeatAgent: heartbeatAgent2 } = (init_agents(), __toCommonJS(exports_agents));
|
|
9391
|
+
const agent = heartbeatAgent2(id);
|
|
9392
|
+
if (!agent) {
|
|
9393
|
+
logError(chalk6.red(`Agent not found: ${id}`));
|
|
9394
|
+
process.exit(1);
|
|
9395
|
+
}
|
|
9396
|
+
log(chalk6.green(`Heartbeat sent for ${agent.name} \u2014 ${agent.lastSeenAt}`));
|
|
9397
|
+
} catch (error) {
|
|
9398
|
+
logError(chalk6.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
9399
|
+
process.exit(1);
|
|
9400
|
+
}
|
|
9401
|
+
});
|
|
9402
|
+
agentCmd.command("focus <agent-id> [scenario-id]").description("Set (or clear) an agent's current focus scenario").action((agentId, scenarioId) => {
|
|
9403
|
+
try {
|
|
9404
|
+
const { setAgentFocus: setAgentFocus2 } = (init_agents(), __toCommonJS(exports_agents));
|
|
9405
|
+
const agent = setAgentFocus2(agentId, scenarioId ?? null);
|
|
9406
|
+
if (!agent) {
|
|
9407
|
+
logError(chalk6.red(`Agent not found: ${agentId}`));
|
|
9408
|
+
process.exit(1);
|
|
9409
|
+
}
|
|
9410
|
+
const focus = agent.metadata?.focus ?? null;
|
|
9411
|
+
log(focus ? chalk6.green(`Agent ${agent.name} focus set to: ${focus}`) : chalk6.dim(`Agent ${agent.name} focus cleared`));
|
|
9412
|
+
} catch (error) {
|
|
9413
|
+
logError(chalk6.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
9414
|
+
process.exit(1);
|
|
9415
|
+
}
|
|
9416
|
+
});
|
|
9417
|
+
agentCmd.command("list").description("List all registered agents").action(() => {
|
|
9418
|
+
try {
|
|
9419
|
+
const { listAgents: listAgents2 } = (init_agents(), __toCommonJS(exports_agents));
|
|
9420
|
+
const agents = listAgents2();
|
|
9421
|
+
if (agents.length === 0) {
|
|
9422
|
+
log(chalk6.dim("No agents registered."));
|
|
9423
|
+
return;
|
|
9424
|
+
}
|
|
9425
|
+
for (const a of agents) {
|
|
9426
|
+
const focus = a.metadata?.focus;
|
|
9427
|
+
log(` ${chalk6.cyan(a.id.slice(0, 8))} ${chalk6.bold(a.name)}${a.role ? chalk6.dim(` [${a.role}]`) : ""}${focus ? chalk6.yellow(` \u2192 ${focus}`) : ""} ${chalk6.dim(a.lastSeenAt)}`);
|
|
9428
|
+
}
|
|
8788
9429
|
} catch (error) {
|
|
8789
|
-
logError(
|
|
9430
|
+
logError(chalk6.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
8790
9431
|
process.exit(1);
|
|
8791
9432
|
}
|
|
8792
9433
|
});
|
|
@@ -8794,9 +9435,9 @@ program2.command("doctor").description("Check system setup and configuration").a
|
|
|
8794
9435
|
let allPassed = true;
|
|
8795
9436
|
const hasApiKey = Boolean(process.env["ANTHROPIC_API_KEY"]);
|
|
8796
9437
|
if (hasApiKey) {
|
|
8797
|
-
log(
|
|
9438
|
+
log(chalk6.green("\u2713") + " ANTHROPIC_API_KEY is set");
|
|
8798
9439
|
} else {
|
|
8799
|
-
log(
|
|
9440
|
+
log(chalk6.red("\u2717") + " ANTHROPIC_API_KEY is not set (required for AI-powered tests)");
|
|
8800
9441
|
allPassed = false;
|
|
8801
9442
|
}
|
|
8802
9443
|
const dbPath = join6(process.env["HOME"] ?? "~", ".testers", "testers.db");
|
|
@@ -8804,9 +9445,9 @@ program2.command("doctor").description("Check system setup and configuration").a
|
|
|
8804
9445
|
const { Database: Database3 } = await import("bun:sqlite");
|
|
8805
9446
|
const db2 = new Database3(dbPath, { create: true });
|
|
8806
9447
|
db2.close();
|
|
8807
|
-
log(
|
|
9448
|
+
log(chalk6.green("\u2713") + ` Database accessible: ${dbPath}`);
|
|
8808
9449
|
} catch (err) {
|
|
8809
|
-
log(
|
|
9450
|
+
log(chalk6.red("\u2717") + ` Database not accessible at ${dbPath}: ${err instanceof Error ? err.message : String(err)}`);
|
|
8810
9451
|
allPassed = false;
|
|
8811
9452
|
}
|
|
8812
9453
|
try {
|
|
@@ -8814,13 +9455,13 @@ program2.command("doctor").description("Check system setup and configuration").a
|
|
|
8814
9455
|
const execPath = chromium4.executablePath();
|
|
8815
9456
|
const { existsSync: fsExists } = await import("fs");
|
|
8816
9457
|
if (fsExists(execPath)) {
|
|
8817
|
-
log(
|
|
9458
|
+
log(chalk6.green("\u2713") + " Playwright chromium is installed");
|
|
8818
9459
|
} else {
|
|
8819
|
-
log(
|
|
9460
|
+
log(chalk6.red("\u2717") + ` Playwright chromium executable not found at ${execPath}. Run: testers install`);
|
|
8820
9461
|
allPassed = false;
|
|
8821
9462
|
}
|
|
8822
9463
|
} catch {
|
|
8823
|
-
log(
|
|
9464
|
+
log(chalk6.red("\u2717") + " Playwright is not installed. Run: testers install");
|
|
8824
9465
|
allPassed = false;
|
|
8825
9466
|
}
|
|
8826
9467
|
if (!allPassed) {
|
|
@@ -8840,7 +9481,7 @@ program2.command("serve").description("Start the Open Testers web dashboard").op
|
|
|
8840
9481
|
stdout: "inherit",
|
|
8841
9482
|
stderr: "inherit"
|
|
8842
9483
|
});
|
|
8843
|
-
log(
|
|
9484
|
+
log(chalk6.green(`Open Testers dashboard starting at ${url}`));
|
|
8844
9485
|
if (opts.open !== false) {
|
|
8845
9486
|
await new Promise((r) => setTimeout(r, 1500));
|
|
8846
9487
|
const openCmd = process.platform === "darwin" ? "open" : "xdg-open";
|
|
@@ -8848,7 +9489,7 @@ program2.command("serve").description("Start the Open Testers web dashboard").op
|
|
|
8848
9489
|
}
|
|
8849
9490
|
await proc.exited;
|
|
8850
9491
|
} catch (error) {
|
|
8851
|
-
logError(
|
|
9492
|
+
logError(chalk6.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
8852
9493
|
process.exit(1);
|
|
8853
9494
|
}
|
|
8854
9495
|
});
|