@hasna/testers 0.0.1 → 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/index.js +762 -13
- package/dist/db/database.d.ts.map +1 -1
- package/dist/db/schedules.d.ts +9 -0
- package/dist/db/schedules.d.ts.map +1 -0
- package/dist/index.d.ts +5 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +651 -7
- package/dist/lib/ai-client.d.ts.map +1 -1
- package/dist/lib/scheduler.d.ts +71 -0
- package/dist/lib/scheduler.d.ts.map +1 -0
- package/dist/mcp/index.js +701 -7
- package/dist/server/index.js +680 -7
- package/dist/types/index.d.ts +80 -0
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -2080,6 +2080,16 @@ var MODEL_MAP = {
|
|
|
2080
2080
|
thorough: "claude-sonnet-4-6-20260311",
|
|
2081
2081
|
deep: "claude-opus-4-6-20260311"
|
|
2082
2082
|
};
|
|
2083
|
+
function projectFromRow(row) {
|
|
2084
|
+
return {
|
|
2085
|
+
id: row.id,
|
|
2086
|
+
name: row.name,
|
|
2087
|
+
path: row.path,
|
|
2088
|
+
description: row.description,
|
|
2089
|
+
createdAt: row.created_at,
|
|
2090
|
+
updatedAt: row.updated_at
|
|
2091
|
+
};
|
|
2092
|
+
}
|
|
2083
2093
|
function scenarioFromRow(row) {
|
|
2084
2094
|
return {
|
|
2085
2095
|
id: row.id,
|
|
@@ -2148,6 +2158,26 @@ function screenshotFromRow(row) {
|
|
|
2148
2158
|
timestamp: row.timestamp
|
|
2149
2159
|
};
|
|
2150
2160
|
}
|
|
2161
|
+
function scheduleFromRow(row) {
|
|
2162
|
+
return {
|
|
2163
|
+
id: row.id,
|
|
2164
|
+
projectId: row.project_id,
|
|
2165
|
+
name: row.name,
|
|
2166
|
+
cronExpression: row.cron_expression,
|
|
2167
|
+
url: row.url,
|
|
2168
|
+
scenarioFilter: JSON.parse(row.scenario_filter),
|
|
2169
|
+
model: row.model,
|
|
2170
|
+
headed: row.headed === 1,
|
|
2171
|
+
parallel: row.parallel,
|
|
2172
|
+
timeoutMs: row.timeout_ms,
|
|
2173
|
+
enabled: row.enabled === 1,
|
|
2174
|
+
lastRunId: row.last_run_id,
|
|
2175
|
+
lastRunAt: row.last_run_at,
|
|
2176
|
+
nextRunAt: row.next_run_at,
|
|
2177
|
+
createdAt: row.created_at,
|
|
2178
|
+
updatedAt: row.updated_at
|
|
2179
|
+
};
|
|
2180
|
+
}
|
|
2151
2181
|
class VersionConflictError extends Error {
|
|
2152
2182
|
constructor(entity, id) {
|
|
2153
2183
|
super(`Version conflict on ${entity}: ${id}`);
|
|
@@ -2175,6 +2205,12 @@ class TodosConnectionError extends Error {
|
|
|
2175
2205
|
this.name = "TodosConnectionError";
|
|
2176
2206
|
}
|
|
2177
2207
|
}
|
|
2208
|
+
class ScheduleNotFoundError extends Error {
|
|
2209
|
+
constructor(id) {
|
|
2210
|
+
super(`Schedule not found: ${id}`);
|
|
2211
|
+
this.name = "ScheduleNotFoundError";
|
|
2212
|
+
}
|
|
2213
|
+
}
|
|
2178
2214
|
|
|
2179
2215
|
// src/db/database.ts
|
|
2180
2216
|
import { Database } from "bun:sqlite";
|
|
@@ -2304,6 +2340,30 @@ var MIGRATIONS = [
|
|
|
2304
2340
|
`
|
|
2305
2341
|
ALTER TABLE projects ADD COLUMN scenario_prefix TEXT DEFAULT 'TST';
|
|
2306
2342
|
ALTER TABLE projects ADD COLUMN scenario_counter INTEGER DEFAULT 0;
|
|
2343
|
+
`,
|
|
2344
|
+
`
|
|
2345
|
+
CREATE TABLE IF NOT EXISTS schedules (
|
|
2346
|
+
id TEXT PRIMARY KEY,
|
|
2347
|
+
project_id TEXT REFERENCES projects(id) ON DELETE CASCADE,
|
|
2348
|
+
name TEXT NOT NULL,
|
|
2349
|
+
cron_expression TEXT NOT NULL,
|
|
2350
|
+
url TEXT NOT NULL,
|
|
2351
|
+
scenario_filter TEXT NOT NULL DEFAULT '{}',
|
|
2352
|
+
model TEXT,
|
|
2353
|
+
headed INTEGER NOT NULL DEFAULT 0,
|
|
2354
|
+
parallel INTEGER NOT NULL DEFAULT 1,
|
|
2355
|
+
timeout_ms INTEGER,
|
|
2356
|
+
enabled INTEGER NOT NULL DEFAULT 1,
|
|
2357
|
+
last_run_id TEXT REFERENCES runs(id) ON DELETE SET NULL,
|
|
2358
|
+
last_run_at TEXT,
|
|
2359
|
+
next_run_at TEXT,
|
|
2360
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
2361
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
2362
|
+
);
|
|
2363
|
+
|
|
2364
|
+
CREATE INDEX IF NOT EXISTS idx_schedules_project ON schedules(project_id);
|
|
2365
|
+
CREATE INDEX IF NOT EXISTS idx_schedules_enabled ON schedules(enabled);
|
|
2366
|
+
CREATE INDEX IF NOT EXISTS idx_schedules_next_run ON schedules(next_run_at);
|
|
2307
2367
|
`
|
|
2308
2368
|
];
|
|
2309
2369
|
function applyMigrations(database) {
|
|
@@ -3036,6 +3096,127 @@ var BROWSER_TOOLS = [
|
|
|
3036
3096
|
required: ["text"]
|
|
3037
3097
|
}
|
|
3038
3098
|
},
|
|
3099
|
+
{
|
|
3100
|
+
name: "scroll",
|
|
3101
|
+
description: "Scroll the page up or down by a given amount of pixels.",
|
|
3102
|
+
input_schema: {
|
|
3103
|
+
type: "object",
|
|
3104
|
+
properties: {
|
|
3105
|
+
direction: {
|
|
3106
|
+
type: "string",
|
|
3107
|
+
enum: ["up", "down"],
|
|
3108
|
+
description: "Direction to scroll."
|
|
3109
|
+
},
|
|
3110
|
+
amount: {
|
|
3111
|
+
type: "number",
|
|
3112
|
+
description: "Number of pixels to scroll (default: 500)."
|
|
3113
|
+
}
|
|
3114
|
+
},
|
|
3115
|
+
required: ["direction"]
|
|
3116
|
+
}
|
|
3117
|
+
},
|
|
3118
|
+
{
|
|
3119
|
+
name: "get_page_html",
|
|
3120
|
+
description: "Get simplified HTML of the page body content, truncated to 8000 characters.",
|
|
3121
|
+
input_schema: {
|
|
3122
|
+
type: "object",
|
|
3123
|
+
properties: {},
|
|
3124
|
+
required: []
|
|
3125
|
+
}
|
|
3126
|
+
},
|
|
3127
|
+
{
|
|
3128
|
+
name: "get_elements",
|
|
3129
|
+
description: "List elements matching a CSS selector with their text, tag name, and key attributes (max 20 results).",
|
|
3130
|
+
input_schema: {
|
|
3131
|
+
type: "object",
|
|
3132
|
+
properties: {
|
|
3133
|
+
selector: {
|
|
3134
|
+
type: "string",
|
|
3135
|
+
description: "CSS selector to match elements."
|
|
3136
|
+
}
|
|
3137
|
+
},
|
|
3138
|
+
required: ["selector"]
|
|
3139
|
+
}
|
|
3140
|
+
},
|
|
3141
|
+
{
|
|
3142
|
+
name: "wait_for_navigation",
|
|
3143
|
+
description: "Wait for page navigation/load to complete (network idle).",
|
|
3144
|
+
input_schema: {
|
|
3145
|
+
type: "object",
|
|
3146
|
+
properties: {
|
|
3147
|
+
timeout: {
|
|
3148
|
+
type: "number",
|
|
3149
|
+
description: "Maximum time to wait in milliseconds (default: 10000)."
|
|
3150
|
+
}
|
|
3151
|
+
},
|
|
3152
|
+
required: []
|
|
3153
|
+
}
|
|
3154
|
+
},
|
|
3155
|
+
{
|
|
3156
|
+
name: "get_page_title",
|
|
3157
|
+
description: "Get the document title of the current page.",
|
|
3158
|
+
input_schema: {
|
|
3159
|
+
type: "object",
|
|
3160
|
+
properties: {},
|
|
3161
|
+
required: []
|
|
3162
|
+
}
|
|
3163
|
+
},
|
|
3164
|
+
{
|
|
3165
|
+
name: "count_elements",
|
|
3166
|
+
description: "Count the number of elements matching a CSS selector.",
|
|
3167
|
+
input_schema: {
|
|
3168
|
+
type: "object",
|
|
3169
|
+
properties: {
|
|
3170
|
+
selector: {
|
|
3171
|
+
type: "string",
|
|
3172
|
+
description: "CSS selector to count matching elements."
|
|
3173
|
+
}
|
|
3174
|
+
},
|
|
3175
|
+
required: ["selector"]
|
|
3176
|
+
}
|
|
3177
|
+
},
|
|
3178
|
+
{
|
|
3179
|
+
name: "hover",
|
|
3180
|
+
description: "Hover over an element matching the given CSS selector.",
|
|
3181
|
+
input_schema: {
|
|
3182
|
+
type: "object",
|
|
3183
|
+
properties: {
|
|
3184
|
+
selector: {
|
|
3185
|
+
type: "string",
|
|
3186
|
+
description: "CSS selector of the element to hover over."
|
|
3187
|
+
}
|
|
3188
|
+
},
|
|
3189
|
+
required: ["selector"]
|
|
3190
|
+
}
|
|
3191
|
+
},
|
|
3192
|
+
{
|
|
3193
|
+
name: "check",
|
|
3194
|
+
description: "Check a checkbox matching the given CSS selector.",
|
|
3195
|
+
input_schema: {
|
|
3196
|
+
type: "object",
|
|
3197
|
+
properties: {
|
|
3198
|
+
selector: {
|
|
3199
|
+
type: "string",
|
|
3200
|
+
description: "CSS selector of the checkbox to check."
|
|
3201
|
+
}
|
|
3202
|
+
},
|
|
3203
|
+
required: ["selector"]
|
|
3204
|
+
}
|
|
3205
|
+
},
|
|
3206
|
+
{
|
|
3207
|
+
name: "uncheck",
|
|
3208
|
+
description: "Uncheck a checkbox matching the given CSS selector.",
|
|
3209
|
+
input_schema: {
|
|
3210
|
+
type: "object",
|
|
3211
|
+
properties: {
|
|
3212
|
+
selector: {
|
|
3213
|
+
type: "string",
|
|
3214
|
+
description: "CSS selector of the checkbox to uncheck."
|
|
3215
|
+
}
|
|
3216
|
+
},
|
|
3217
|
+
required: ["selector"]
|
|
3218
|
+
}
|
|
3219
|
+
},
|
|
3039
3220
|
{
|
|
3040
3221
|
name: "report_result",
|
|
3041
3222
|
description: "Report the final test result. Call this when you have completed testing the scenario. This MUST be the last tool you call.",
|
|
@@ -3167,6 +3348,113 @@ async function executeTool(page, screenshotter, toolName, toolInput, context) {
|
|
|
3167
3348
|
return { result: "false" };
|
|
3168
3349
|
}
|
|
3169
3350
|
}
|
|
3351
|
+
case "scroll": {
|
|
3352
|
+
const direction = toolInput.direction;
|
|
3353
|
+
const amount = typeof toolInput.amount === "number" ? toolInput.amount : 500;
|
|
3354
|
+
const scrollY = direction === "down" ? amount : -amount;
|
|
3355
|
+
await page.evaluate((y) => window.scrollBy(0, y), scrollY);
|
|
3356
|
+
const screenshot = await screenshotter.capture(page, {
|
|
3357
|
+
runId: context.runId,
|
|
3358
|
+
scenarioSlug: context.scenarioSlug,
|
|
3359
|
+
stepNumber: context.stepNumber,
|
|
3360
|
+
action: "scroll"
|
|
3361
|
+
});
|
|
3362
|
+
return {
|
|
3363
|
+
result: `Scrolled ${direction} by ${amount}px`,
|
|
3364
|
+
screenshot
|
|
3365
|
+
};
|
|
3366
|
+
}
|
|
3367
|
+
case "get_page_html": {
|
|
3368
|
+
const html = await page.evaluate(() => document.body.innerHTML);
|
|
3369
|
+
const truncated = html.length > 8000 ? html.slice(0, 8000) + "..." : html;
|
|
3370
|
+
return {
|
|
3371
|
+
result: truncated
|
|
3372
|
+
};
|
|
3373
|
+
}
|
|
3374
|
+
case "get_elements": {
|
|
3375
|
+
const selector = toolInput.selector;
|
|
3376
|
+
const allElements = await page.locator(selector).all();
|
|
3377
|
+
const elements = allElements.slice(0, 20);
|
|
3378
|
+
const results = [];
|
|
3379
|
+
for (let i = 0;i < elements.length; i++) {
|
|
3380
|
+
const el = elements[i];
|
|
3381
|
+
const tagName = await el.evaluate((e) => e.tagName.toLowerCase());
|
|
3382
|
+
const textContent = await el.textContent() ?? "";
|
|
3383
|
+
const trimmedText = textContent.trim().slice(0, 100);
|
|
3384
|
+
const id = await el.getAttribute("id");
|
|
3385
|
+
const className = await el.getAttribute("class");
|
|
3386
|
+
const href = await el.getAttribute("href");
|
|
3387
|
+
const type = await el.getAttribute("type");
|
|
3388
|
+
const placeholder = await el.getAttribute("placeholder");
|
|
3389
|
+
const ariaLabel = await el.getAttribute("aria-label");
|
|
3390
|
+
const attrs = [];
|
|
3391
|
+
if (id)
|
|
3392
|
+
attrs.push(`id="${id}"`);
|
|
3393
|
+
if (className)
|
|
3394
|
+
attrs.push(`class="${className}"`);
|
|
3395
|
+
if (href)
|
|
3396
|
+
attrs.push(`href="${href}"`);
|
|
3397
|
+
if (type)
|
|
3398
|
+
attrs.push(`type="${type}"`);
|
|
3399
|
+
if (placeholder)
|
|
3400
|
+
attrs.push(`placeholder="${placeholder}"`);
|
|
3401
|
+
if (ariaLabel)
|
|
3402
|
+
attrs.push(`aria-label="${ariaLabel}"`);
|
|
3403
|
+
results.push(`[${i}] <${tagName}${attrs.length ? " " + attrs.join(" ") : ""}> ${trimmedText}`);
|
|
3404
|
+
}
|
|
3405
|
+
return {
|
|
3406
|
+
result: results.length > 0 ? results.join(`
|
|
3407
|
+
`) : `No elements found matching "${selector}"`
|
|
3408
|
+
};
|
|
3409
|
+
}
|
|
3410
|
+
case "wait_for_navigation": {
|
|
3411
|
+
const timeout = typeof toolInput.timeout === "number" ? toolInput.timeout : 1e4;
|
|
3412
|
+
await page.waitForLoadState("networkidle", { timeout });
|
|
3413
|
+
return {
|
|
3414
|
+
result: "Navigation/load completed"
|
|
3415
|
+
};
|
|
3416
|
+
}
|
|
3417
|
+
case "get_page_title": {
|
|
3418
|
+
const title = await page.title();
|
|
3419
|
+
return {
|
|
3420
|
+
result: title || "(no title)"
|
|
3421
|
+
};
|
|
3422
|
+
}
|
|
3423
|
+
case "count_elements": {
|
|
3424
|
+
const selector = toolInput.selector;
|
|
3425
|
+
const count = await page.locator(selector).count();
|
|
3426
|
+
return {
|
|
3427
|
+
result: `${count} element(s) matching "${selector}"`
|
|
3428
|
+
};
|
|
3429
|
+
}
|
|
3430
|
+
case "hover": {
|
|
3431
|
+
const selector = toolInput.selector;
|
|
3432
|
+
await page.hover(selector);
|
|
3433
|
+
const screenshot = await screenshotter.capture(page, {
|
|
3434
|
+
runId: context.runId,
|
|
3435
|
+
scenarioSlug: context.scenarioSlug,
|
|
3436
|
+
stepNumber: context.stepNumber,
|
|
3437
|
+
action: "hover"
|
|
3438
|
+
});
|
|
3439
|
+
return {
|
|
3440
|
+
result: `Hovered over: ${selector}`,
|
|
3441
|
+
screenshot
|
|
3442
|
+
};
|
|
3443
|
+
}
|
|
3444
|
+
case "check": {
|
|
3445
|
+
const selector = toolInput.selector;
|
|
3446
|
+
await page.check(selector);
|
|
3447
|
+
return {
|
|
3448
|
+
result: `Checked checkbox: ${selector}`
|
|
3449
|
+
};
|
|
3450
|
+
}
|
|
3451
|
+
case "uncheck": {
|
|
3452
|
+
const selector = toolInput.selector;
|
|
3453
|
+
await page.uncheck(selector);
|
|
3454
|
+
return {
|
|
3455
|
+
result: `Unchecked checkbox: ${selector}`
|
|
3456
|
+
};
|
|
3457
|
+
}
|
|
3170
3458
|
case "report_result": {
|
|
3171
3459
|
const status = toolInput.status;
|
|
3172
3460
|
const reasoning = toolInput.reasoning;
|
|
@@ -3193,13 +3481,26 @@ async function runAgentLoop(options) {
|
|
|
3193
3481
|
maxTurns = 30
|
|
3194
3482
|
} = options;
|
|
3195
3483
|
const systemPrompt = [
|
|
3196
|
-
"You are
|
|
3197
|
-
"
|
|
3198
|
-
"
|
|
3199
|
-
"
|
|
3200
|
-
"
|
|
3201
|
-
"
|
|
3202
|
-
|
|
3484
|
+
"You are an expert QA testing agent. Your job is to thoroughly test web application scenarios.",
|
|
3485
|
+
"You have browser tools to navigate, interact with, and inspect web pages.",
|
|
3486
|
+
"",
|
|
3487
|
+
"Strategy:",
|
|
3488
|
+
"1. First navigate to the target page and take a screenshot to understand the layout",
|
|
3489
|
+
"2. If you can't find an element, use get_elements or get_page_html to discover selectors",
|
|
3490
|
+
"3. Use scroll to discover content below the fold",
|
|
3491
|
+
"4. Use wait_for or wait_for_navigation after actions that trigger page loads",
|
|
3492
|
+
"5. Take screenshots after every meaningful state change",
|
|
3493
|
+
"6. Use assert_text and assert_visible to verify expected outcomes",
|
|
3494
|
+
"7. When done testing, call report_result with detailed pass/fail reasoning",
|
|
3495
|
+
"",
|
|
3496
|
+
"Tips:",
|
|
3497
|
+
"- Try multiple selector strategies: by text, by role, by class, by id",
|
|
3498
|
+
"- If a click triggers navigation, use wait_for_navigation after",
|
|
3499
|
+
"- For forms, fill all fields before submitting",
|
|
3500
|
+
"- Check for error messages after form submissions",
|
|
3501
|
+
"- Verify both positive and negative states"
|
|
3502
|
+
].join(`
|
|
3503
|
+
`);
|
|
3203
3504
|
const userParts = [
|
|
3204
3505
|
`**Scenario:** ${scenario.name}`,
|
|
3205
3506
|
`**Description:** ${scenario.description}`
|
|
@@ -3765,17 +4066,179 @@ function importFromTodos(options = {}) {
|
|
|
3765
4066
|
return { imported, skipped };
|
|
3766
4067
|
}
|
|
3767
4068
|
|
|
4069
|
+
// src/db/projects.ts
|
|
4070
|
+
function createProject(input) {
|
|
4071
|
+
const db2 = getDatabase();
|
|
4072
|
+
const id = uuid();
|
|
4073
|
+
const timestamp = now();
|
|
4074
|
+
db2.query(`
|
|
4075
|
+
INSERT INTO projects (id, name, path, description, created_at, updated_at)
|
|
4076
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
4077
|
+
`).run(id, input.name, input.path ?? null, input.description ?? null, timestamp, timestamp);
|
|
4078
|
+
return getProject(id);
|
|
4079
|
+
}
|
|
4080
|
+
function getProject(id) {
|
|
4081
|
+
const db2 = getDatabase();
|
|
4082
|
+
const row = db2.query("SELECT * FROM projects WHERE id = ?").get(id);
|
|
4083
|
+
return row ? projectFromRow(row) : null;
|
|
4084
|
+
}
|
|
4085
|
+
function listProjects() {
|
|
4086
|
+
const db2 = getDatabase();
|
|
4087
|
+
const rows = db2.query("SELECT * FROM projects ORDER BY created_at DESC").all();
|
|
4088
|
+
return rows.map(projectFromRow);
|
|
4089
|
+
}
|
|
4090
|
+
function ensureProject(name, path) {
|
|
4091
|
+
const db2 = getDatabase();
|
|
4092
|
+
const byPath = db2.query("SELECT * FROM projects WHERE path = ?").get(path);
|
|
4093
|
+
if (byPath)
|
|
4094
|
+
return projectFromRow(byPath);
|
|
4095
|
+
const byName = db2.query("SELECT * FROM projects WHERE name = ?").get(name);
|
|
4096
|
+
if (byName)
|
|
4097
|
+
return projectFromRow(byName);
|
|
4098
|
+
return createProject({ name, path });
|
|
4099
|
+
}
|
|
4100
|
+
|
|
4101
|
+
// src/db/schedules.ts
|
|
4102
|
+
function createSchedule(input) {
|
|
4103
|
+
const db2 = getDatabase();
|
|
4104
|
+
const id = uuid();
|
|
4105
|
+
const timestamp = now();
|
|
4106
|
+
db2.query(`
|
|
4107
|
+
INSERT INTO schedules (id, project_id, name, cron_expression, url, scenario_filter, model, headed, parallel, timeout_ms, enabled, created_at, updated_at)
|
|
4108
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?, ?)
|
|
4109
|
+
`).run(id, input.projectId ?? null, input.name, input.cronExpression, input.url, JSON.stringify(input.scenarioFilter ?? {}), input.model ?? null, input.headed ? 1 : 0, input.parallel ?? 1, input.timeoutMs ?? null, timestamp, timestamp);
|
|
4110
|
+
return getSchedule(id);
|
|
4111
|
+
}
|
|
4112
|
+
function getSchedule(id) {
|
|
4113
|
+
const db2 = getDatabase();
|
|
4114
|
+
let row = db2.query("SELECT * FROM schedules WHERE id = ?").get(id);
|
|
4115
|
+
if (row)
|
|
4116
|
+
return scheduleFromRow(row);
|
|
4117
|
+
const fullId = resolvePartialId("schedules", id);
|
|
4118
|
+
if (fullId) {
|
|
4119
|
+
row = db2.query("SELECT * FROM schedules WHERE id = ?").get(fullId);
|
|
4120
|
+
if (row)
|
|
4121
|
+
return scheduleFromRow(row);
|
|
4122
|
+
}
|
|
4123
|
+
return null;
|
|
4124
|
+
}
|
|
4125
|
+
function listSchedules(filter) {
|
|
4126
|
+
const db2 = getDatabase();
|
|
4127
|
+
const conditions = [];
|
|
4128
|
+
const params = [];
|
|
4129
|
+
if (filter?.projectId) {
|
|
4130
|
+
conditions.push("project_id = ?");
|
|
4131
|
+
params.push(filter.projectId);
|
|
4132
|
+
}
|
|
4133
|
+
if (filter?.enabled !== undefined) {
|
|
4134
|
+
conditions.push("enabled = ?");
|
|
4135
|
+
params.push(filter.enabled ? 1 : 0);
|
|
4136
|
+
}
|
|
4137
|
+
let sql = "SELECT * FROM schedules";
|
|
4138
|
+
if (conditions.length > 0) {
|
|
4139
|
+
sql += " WHERE " + conditions.join(" AND ");
|
|
4140
|
+
}
|
|
4141
|
+
sql += " ORDER BY created_at DESC";
|
|
4142
|
+
if (filter?.limit) {
|
|
4143
|
+
sql += " LIMIT ?";
|
|
4144
|
+
params.push(filter.limit);
|
|
4145
|
+
}
|
|
4146
|
+
if (filter?.offset) {
|
|
4147
|
+
sql += " OFFSET ?";
|
|
4148
|
+
params.push(filter.offset);
|
|
4149
|
+
}
|
|
4150
|
+
const rows = db2.query(sql).all(...params);
|
|
4151
|
+
return rows.map(scheduleFromRow);
|
|
4152
|
+
}
|
|
4153
|
+
function updateSchedule(id, input) {
|
|
4154
|
+
const db2 = getDatabase();
|
|
4155
|
+
const existing = getSchedule(id);
|
|
4156
|
+
if (!existing) {
|
|
4157
|
+
throw new ScheduleNotFoundError(id);
|
|
4158
|
+
}
|
|
4159
|
+
const sets = [];
|
|
4160
|
+
const params = [];
|
|
4161
|
+
if (input.name !== undefined) {
|
|
4162
|
+
sets.push("name = ?");
|
|
4163
|
+
params.push(input.name);
|
|
4164
|
+
}
|
|
4165
|
+
if (input.cronExpression !== undefined) {
|
|
4166
|
+
sets.push("cron_expression = ?");
|
|
4167
|
+
params.push(input.cronExpression);
|
|
4168
|
+
}
|
|
4169
|
+
if (input.url !== undefined) {
|
|
4170
|
+
sets.push("url = ?");
|
|
4171
|
+
params.push(input.url);
|
|
4172
|
+
}
|
|
4173
|
+
if (input.scenarioFilter !== undefined) {
|
|
4174
|
+
sets.push("scenario_filter = ?");
|
|
4175
|
+
params.push(JSON.stringify(input.scenarioFilter));
|
|
4176
|
+
}
|
|
4177
|
+
if (input.model !== undefined) {
|
|
4178
|
+
sets.push("model = ?");
|
|
4179
|
+
params.push(input.model);
|
|
4180
|
+
}
|
|
4181
|
+
if (input.headed !== undefined) {
|
|
4182
|
+
sets.push("headed = ?");
|
|
4183
|
+
params.push(input.headed ? 1 : 0);
|
|
4184
|
+
}
|
|
4185
|
+
if (input.parallel !== undefined) {
|
|
4186
|
+
sets.push("parallel = ?");
|
|
4187
|
+
params.push(input.parallel);
|
|
4188
|
+
}
|
|
4189
|
+
if (input.timeoutMs !== undefined) {
|
|
4190
|
+
sets.push("timeout_ms = ?");
|
|
4191
|
+
params.push(input.timeoutMs);
|
|
4192
|
+
}
|
|
4193
|
+
if (input.enabled !== undefined) {
|
|
4194
|
+
sets.push("enabled = ?");
|
|
4195
|
+
params.push(input.enabled ? 1 : 0);
|
|
4196
|
+
}
|
|
4197
|
+
if (sets.length === 0) {
|
|
4198
|
+
return existing;
|
|
4199
|
+
}
|
|
4200
|
+
sets.push("updated_at = ?");
|
|
4201
|
+
params.push(now());
|
|
4202
|
+
params.push(existing.id);
|
|
4203
|
+
db2.query(`UPDATE schedules SET ${sets.join(", ")} WHERE id = ?`).run(...params);
|
|
4204
|
+
return getSchedule(existing.id);
|
|
4205
|
+
}
|
|
4206
|
+
function deleteSchedule(id) {
|
|
4207
|
+
const db2 = getDatabase();
|
|
4208
|
+
const schedule = getSchedule(id);
|
|
4209
|
+
if (!schedule)
|
|
4210
|
+
return false;
|
|
4211
|
+
const result = db2.query("DELETE FROM schedules WHERE id = ?").run(schedule.id);
|
|
4212
|
+
return result.changes > 0;
|
|
4213
|
+
}
|
|
4214
|
+
|
|
3768
4215
|
// src/cli/index.tsx
|
|
4216
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync3 } from "fs";
|
|
3769
4217
|
var program2 = new Command;
|
|
3770
4218
|
program2.name("testers").version("0.0.1").description("AI-powered browser testing CLI");
|
|
4219
|
+
var CONFIG_DIR2 = join5(process.env["HOME"] ?? "~", ".testers");
|
|
4220
|
+
var CONFIG_PATH2 = join5(CONFIG_DIR2, "config.json");
|
|
4221
|
+
function getActiveProject() {
|
|
4222
|
+
try {
|
|
4223
|
+
if (existsSync5(CONFIG_PATH2)) {
|
|
4224
|
+
const raw = JSON.parse(readFileSync2(CONFIG_PATH2, "utf-8"));
|
|
4225
|
+
return raw.activeProject ?? undefined;
|
|
4226
|
+
}
|
|
4227
|
+
} catch {}
|
|
4228
|
+
return;
|
|
4229
|
+
}
|
|
4230
|
+
function resolveProject(optProject) {
|
|
4231
|
+
return optProject ?? getActiveProject();
|
|
4232
|
+
}
|
|
3771
4233
|
program2.command("add <name>").description("Create a new test scenario").option("-d, --description <text>", "Scenario description", "").option("-s, --steps <step>", "Test step (repeatable)", (val, acc) => {
|
|
3772
4234
|
acc.push(val);
|
|
3773
4235
|
return acc;
|
|
3774
4236
|
}, []).option("-t, --tag <tag>", "Tag (repeatable)", (val, acc) => {
|
|
3775
4237
|
acc.push(val);
|
|
3776
4238
|
return acc;
|
|
3777
|
-
}, []).option("-p, --priority <level>", "Priority level", "medium").option("-m, --model <model>", "AI model to use").option("--path <path>", "Target path on the URL").option("--auth", "Requires authentication", false).option("--timeout <ms>", "Timeout in milliseconds").action((name, opts) => {
|
|
4239
|
+
}, []).option("-p, --priority <level>", "Priority level", "medium").option("-m, --model <model>", "AI model to use").option("--path <path>", "Target path on the URL").option("--auth", "Requires authentication", false).option("--timeout <ms>", "Timeout in milliseconds").option("--project <id>", "Project ID").action((name, opts) => {
|
|
3778
4240
|
try {
|
|
4241
|
+
const projectId = resolveProject(opts.project);
|
|
3779
4242
|
const scenario = createScenario({
|
|
3780
4243
|
name,
|
|
3781
4244
|
description: opts.description || name,
|
|
@@ -3785,7 +4248,8 @@ program2.command("add <name>").description("Create a new test scenario").option(
|
|
|
3785
4248
|
model: opts.model,
|
|
3786
4249
|
targetPath: opts.path,
|
|
3787
4250
|
requiresAuth: opts.auth,
|
|
3788
|
-
timeoutMs: opts.timeout ? parseInt(opts.timeout, 10) : undefined
|
|
4251
|
+
timeoutMs: opts.timeout ? parseInt(opts.timeout, 10) : undefined,
|
|
4252
|
+
projectId
|
|
3789
4253
|
});
|
|
3790
4254
|
console.log(chalk2.green(`Created scenario ${chalk2.bold(scenario.shortId)}: ${scenario.name}`));
|
|
3791
4255
|
} catch (error) {
|
|
@@ -3892,12 +4356,13 @@ program2.command("run <url> [description]").description("Run test scenarios agai
|
|
|
3892
4356
|
return acc;
|
|
3893
4357
|
}, []).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").action(async (url, description, opts) => {
|
|
3894
4358
|
try {
|
|
4359
|
+
const projectId = resolveProject(opts.project);
|
|
3895
4360
|
if (description) {
|
|
3896
4361
|
const scenario = createScenario({
|
|
3897
4362
|
name: description,
|
|
3898
4363
|
description,
|
|
3899
4364
|
tags: ["ad-hoc"],
|
|
3900
|
-
projectId
|
|
4365
|
+
projectId
|
|
3901
4366
|
});
|
|
3902
4367
|
const { run: run2, results: results2 } = await runByFilter({
|
|
3903
4368
|
url,
|
|
@@ -3906,7 +4371,7 @@ program2.command("run <url> [description]").description("Run test scenarios agai
|
|
|
3906
4371
|
headed: opts.headed,
|
|
3907
4372
|
parallel: parseInt(opts.parallel, 10),
|
|
3908
4373
|
timeout: opts.timeout ? parseInt(opts.timeout, 10) : undefined,
|
|
3909
|
-
projectId
|
|
4374
|
+
projectId
|
|
3910
4375
|
});
|
|
3911
4376
|
if (opts.json || opts.output) {
|
|
3912
4377
|
const jsonOutput = formatJSON(run2, results2);
|
|
@@ -3923,7 +4388,7 @@ program2.command("run <url> [description]").description("Run test scenarios agai
|
|
|
3923
4388
|
process.exit(getExitCode(run2));
|
|
3924
4389
|
}
|
|
3925
4390
|
if (opts.fromTodos) {
|
|
3926
|
-
const result = importFromTodos({ projectId
|
|
4391
|
+
const result = importFromTodos({ projectId });
|
|
3927
4392
|
console.log(chalk2.blue(`Imported ${result.imported} scenarios from todos (${result.skipped} skipped)`));
|
|
3928
4393
|
}
|
|
3929
4394
|
const { run, results } = await runByFilter({
|
|
@@ -3935,7 +4400,7 @@ program2.command("run <url> [description]").description("Run test scenarios agai
|
|
|
3935
4400
|
headed: opts.headed,
|
|
3936
4401
|
parallel: parseInt(opts.parallel, 10),
|
|
3937
4402
|
timeout: opts.timeout ? parseInt(opts.timeout, 10) : undefined,
|
|
3938
|
-
projectId
|
|
4403
|
+
projectId
|
|
3939
4404
|
});
|
|
3940
4405
|
if (opts.json || opts.output) {
|
|
3941
4406
|
const jsonOutput = formatJSON(run, results);
|
|
@@ -4109,4 +4574,288 @@ program2.command("install-browser").description("Install Playwright Chromium bro
|
|
|
4109
4574
|
process.exit(1);
|
|
4110
4575
|
}
|
|
4111
4576
|
});
|
|
4577
|
+
var projectCmd = program2.command("project").description("Manage test projects");
|
|
4578
|
+
projectCmd.command("create <name>").description("Create a new project").option("--path <path>", "Project path").option("-d, --description <text>", "Project description").option("--prefix <prefix>", "Scenario prefix", "TST").action((name, opts) => {
|
|
4579
|
+
try {
|
|
4580
|
+
const project = createProject({
|
|
4581
|
+
name,
|
|
4582
|
+
path: opts.path,
|
|
4583
|
+
description: opts.description
|
|
4584
|
+
});
|
|
4585
|
+
console.log(chalk2.green(`Created project ${chalk2.bold(project.name)} (${project.id})`));
|
|
4586
|
+
} catch (error) {
|
|
4587
|
+
console.error(chalk2.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
4588
|
+
process.exit(1);
|
|
4589
|
+
}
|
|
4590
|
+
});
|
|
4591
|
+
projectCmd.command("list").description("List all projects").action(() => {
|
|
4592
|
+
try {
|
|
4593
|
+
const projects = listProjects();
|
|
4594
|
+
if (projects.length === 0) {
|
|
4595
|
+
console.log(chalk2.dim("No projects found."));
|
|
4596
|
+
return;
|
|
4597
|
+
}
|
|
4598
|
+
console.log("");
|
|
4599
|
+
console.log(chalk2.bold(" Projects"));
|
|
4600
|
+
console.log("");
|
|
4601
|
+
console.log(` ${"ID".padEnd(38)} ${"Name".padEnd(24)} ${"Path".padEnd(30)} Created`);
|
|
4602
|
+
console.log(` ${"\u2500".repeat(38)} ${"\u2500".repeat(24)} ${"\u2500".repeat(30)} ${"\u2500".repeat(20)}`);
|
|
4603
|
+
for (const p of projects) {
|
|
4604
|
+
console.log(` ${p.id.padEnd(38)} ${p.name.padEnd(24)} ${(p.path ?? chalk2.dim("\u2014")).toString().padEnd(30)} ${p.createdAt}`);
|
|
4605
|
+
}
|
|
4606
|
+
console.log("");
|
|
4607
|
+
} catch (error) {
|
|
4608
|
+
console.error(chalk2.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
4609
|
+
process.exit(1);
|
|
4610
|
+
}
|
|
4611
|
+
});
|
|
4612
|
+
projectCmd.command("show <id>").description("Show project details").action((id) => {
|
|
4613
|
+
try {
|
|
4614
|
+
const project = getProject(id);
|
|
4615
|
+
if (!project) {
|
|
4616
|
+
console.error(chalk2.red(`Project not found: ${id}`));
|
|
4617
|
+
process.exit(1);
|
|
4618
|
+
}
|
|
4619
|
+
console.log("");
|
|
4620
|
+
console.log(chalk2.bold(` Project: ${project.name}`));
|
|
4621
|
+
console.log(` ID: ${project.id}`);
|
|
4622
|
+
console.log(` Path: ${project.path ?? chalk2.dim("none")}`);
|
|
4623
|
+
console.log(` Description: ${project.description ?? chalk2.dim("none")}`);
|
|
4624
|
+
console.log(` Created: ${project.createdAt}`);
|
|
4625
|
+
console.log(` Updated: ${project.updatedAt}`);
|
|
4626
|
+
console.log("");
|
|
4627
|
+
} catch (error) {
|
|
4628
|
+
console.error(chalk2.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
4629
|
+
process.exit(1);
|
|
4630
|
+
}
|
|
4631
|
+
});
|
|
4632
|
+
projectCmd.command("use <name>").description("Set active project (find or create)").action((name) => {
|
|
4633
|
+
try {
|
|
4634
|
+
const project = ensureProject(name, process.cwd());
|
|
4635
|
+
if (!existsSync5(CONFIG_DIR2)) {
|
|
4636
|
+
mkdirSync3(CONFIG_DIR2, { recursive: true });
|
|
4637
|
+
}
|
|
4638
|
+
let config = {};
|
|
4639
|
+
if (existsSync5(CONFIG_PATH2)) {
|
|
4640
|
+
try {
|
|
4641
|
+
config = JSON.parse(readFileSync2(CONFIG_PATH2, "utf-8"));
|
|
4642
|
+
} catch {}
|
|
4643
|
+
}
|
|
4644
|
+
config.activeProject = project.id;
|
|
4645
|
+
writeFileSync(CONFIG_PATH2, JSON.stringify(config, null, 2), "utf-8");
|
|
4646
|
+
console.log(chalk2.green(`Active project set to ${chalk2.bold(project.name)} (${project.id})`));
|
|
4647
|
+
} catch (error) {
|
|
4648
|
+
console.error(chalk2.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
4649
|
+
process.exit(1);
|
|
4650
|
+
}
|
|
4651
|
+
});
|
|
4652
|
+
var scheduleCmd = program2.command("schedule").description("Manage recurring test schedules");
|
|
4653
|
+
scheduleCmd.command("create <name>").description("Create a new schedule").requiredOption("--cron <expression>", "Cron expression").requiredOption("--url <url>", "Target URL").option("-t, --tag <tag>", "Tag filter (repeatable)", (val, acc) => {
|
|
4654
|
+
acc.push(val);
|
|
4655
|
+
return acc;
|
|
4656
|
+
}, []).option("-p, --priority <level>", "Priority filter").option("-m, --model <model>", "AI model to use").option("--parallel <n>", "Parallel browsers", "1").option("--headed", "Run in headed mode", false).option("--timeout <ms>", "Timeout in milliseconds").option("--project <id>", "Project ID").action((name, opts) => {
|
|
4657
|
+
try {
|
|
4658
|
+
const projectId = resolveProject(opts.project);
|
|
4659
|
+
const schedule = createSchedule({
|
|
4660
|
+
name,
|
|
4661
|
+
cronExpression: opts.cron,
|
|
4662
|
+
url: opts.url,
|
|
4663
|
+
scenarioFilter: {
|
|
4664
|
+
tags: opts.tag.length > 0 ? opts.tag : undefined,
|
|
4665
|
+
priority: opts.priority
|
|
4666
|
+
},
|
|
4667
|
+
model: opts.model,
|
|
4668
|
+
headed: opts.headed,
|
|
4669
|
+
parallel: parseInt(opts.parallel, 10),
|
|
4670
|
+
timeoutMs: opts.timeout ? parseInt(opts.timeout, 10) : undefined,
|
|
4671
|
+
projectId
|
|
4672
|
+
});
|
|
4673
|
+
console.log(chalk2.green(`Created schedule ${chalk2.bold(schedule.name)} (${schedule.id})`));
|
|
4674
|
+
if (schedule.nextRunAt) {
|
|
4675
|
+
console.log(chalk2.dim(` Next run at: ${schedule.nextRunAt}`));
|
|
4676
|
+
}
|
|
4677
|
+
} catch (error) {
|
|
4678
|
+
console.error(chalk2.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
4679
|
+
process.exit(1);
|
|
4680
|
+
}
|
|
4681
|
+
});
|
|
4682
|
+
scheduleCmd.command("list").description("List schedules").option("--project <id>", "Filter by project ID").option("--enabled", "Show only enabled schedules").action((opts) => {
|
|
4683
|
+
try {
|
|
4684
|
+
const projectId = resolveProject(opts.project);
|
|
4685
|
+
const schedules = listSchedules({
|
|
4686
|
+
projectId,
|
|
4687
|
+
enabled: opts.enabled ? true : undefined
|
|
4688
|
+
});
|
|
4689
|
+
if (schedules.length === 0) {
|
|
4690
|
+
console.log(chalk2.dim("No schedules found."));
|
|
4691
|
+
return;
|
|
4692
|
+
}
|
|
4693
|
+
console.log("");
|
|
4694
|
+
console.log(chalk2.bold(" Schedules"));
|
|
4695
|
+
console.log("");
|
|
4696
|
+
console.log(` ${"Name".padEnd(20)} ${"Cron".padEnd(18)} ${"URL".padEnd(30)} ${"Enabled".padEnd(9)} ${"Next Run".padEnd(22)} Last Run`);
|
|
4697
|
+
console.log(` ${"\u2500".repeat(20)} ${"\u2500".repeat(18)} ${"\u2500".repeat(30)} ${"\u2500".repeat(9)} ${"\u2500".repeat(22)} ${"\u2500".repeat(22)}`);
|
|
4698
|
+
for (const s of schedules) {
|
|
4699
|
+
const enabled = s.enabled ? chalk2.green("yes") : chalk2.red("no");
|
|
4700
|
+
const nextRun = s.nextRunAt ?? chalk2.dim("\u2014");
|
|
4701
|
+
const lastRun = s.lastRunAt ?? chalk2.dim("\u2014");
|
|
4702
|
+
console.log(` ${s.name.padEnd(20)} ${s.cronExpression.padEnd(18)} ${s.url.padEnd(30)} ${enabled.toString().padEnd(9)} ${nextRun.toString().padEnd(22)} ${lastRun}`);
|
|
4703
|
+
}
|
|
4704
|
+
console.log("");
|
|
4705
|
+
} catch (error) {
|
|
4706
|
+
console.error(chalk2.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
4707
|
+
process.exit(1);
|
|
4708
|
+
}
|
|
4709
|
+
});
|
|
4710
|
+
scheduleCmd.command("show <id>").description("Show schedule details").action((id) => {
|
|
4711
|
+
try {
|
|
4712
|
+
const schedule = getSchedule(id);
|
|
4713
|
+
if (!schedule) {
|
|
4714
|
+
console.error(chalk2.red(`Schedule not found: ${id}`));
|
|
4715
|
+
process.exit(1);
|
|
4716
|
+
}
|
|
4717
|
+
console.log("");
|
|
4718
|
+
console.log(chalk2.bold(` Schedule: ${schedule.name}`));
|
|
4719
|
+
console.log(` ID: ${schedule.id}`);
|
|
4720
|
+
console.log(` Cron: ${schedule.cronExpression}`);
|
|
4721
|
+
console.log(` URL: ${schedule.url}`);
|
|
4722
|
+
console.log(` Enabled: ${schedule.enabled ? chalk2.green("yes") : chalk2.red("no")}`);
|
|
4723
|
+
console.log(` Model: ${schedule.model ?? chalk2.dim("default")}`);
|
|
4724
|
+
console.log(` Headed: ${schedule.headed ? "yes" : "no"}`);
|
|
4725
|
+
console.log(` Parallel: ${schedule.parallel}`);
|
|
4726
|
+
console.log(` Timeout: ${schedule.timeoutMs ? `${schedule.timeoutMs}ms` : chalk2.dim("default")}`);
|
|
4727
|
+
console.log(` Project: ${schedule.projectId ?? chalk2.dim("none")}`);
|
|
4728
|
+
console.log(` Filter: ${JSON.stringify(schedule.scenarioFilter)}`);
|
|
4729
|
+
console.log(` Next run: ${schedule.nextRunAt ?? chalk2.dim("not scheduled")}`);
|
|
4730
|
+
console.log(` Last run: ${schedule.lastRunAt ?? chalk2.dim("never")}`);
|
|
4731
|
+
console.log(` Last run ID: ${schedule.lastRunId ?? chalk2.dim("none")}`);
|
|
4732
|
+
console.log(` Created: ${schedule.createdAt}`);
|
|
4733
|
+
console.log(` Updated: ${schedule.updatedAt}`);
|
|
4734
|
+
console.log("");
|
|
4735
|
+
} catch (error) {
|
|
4736
|
+
console.error(chalk2.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
4737
|
+
process.exit(1);
|
|
4738
|
+
}
|
|
4739
|
+
});
|
|
4740
|
+
scheduleCmd.command("enable <id>").description("Enable a schedule").action((id) => {
|
|
4741
|
+
try {
|
|
4742
|
+
const schedule = updateSchedule(id, { enabled: true });
|
|
4743
|
+
console.log(chalk2.green(`Enabled schedule ${chalk2.bold(schedule.name)}`));
|
|
4744
|
+
} catch (error) {
|
|
4745
|
+
console.error(chalk2.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
4746
|
+
process.exit(1);
|
|
4747
|
+
}
|
|
4748
|
+
});
|
|
4749
|
+
scheduleCmd.command("disable <id>").description("Disable a schedule").action((id) => {
|
|
4750
|
+
try {
|
|
4751
|
+
const schedule = updateSchedule(id, { enabled: false });
|
|
4752
|
+
console.log(chalk2.green(`Disabled schedule ${chalk2.bold(schedule.name)}`));
|
|
4753
|
+
} catch (error) {
|
|
4754
|
+
console.error(chalk2.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
4755
|
+
process.exit(1);
|
|
4756
|
+
}
|
|
4757
|
+
});
|
|
4758
|
+
scheduleCmd.command("delete <id>").description("Delete a schedule").action((id) => {
|
|
4759
|
+
try {
|
|
4760
|
+
const deleted = deleteSchedule(id);
|
|
4761
|
+
if (deleted) {
|
|
4762
|
+
console.log(chalk2.green(`Deleted schedule: ${id}`));
|
|
4763
|
+
} else {
|
|
4764
|
+
console.error(chalk2.red(`Schedule not found: ${id}`));
|
|
4765
|
+
process.exit(1);
|
|
4766
|
+
}
|
|
4767
|
+
} catch (error) {
|
|
4768
|
+
console.error(chalk2.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
4769
|
+
process.exit(1);
|
|
4770
|
+
}
|
|
4771
|
+
});
|
|
4772
|
+
scheduleCmd.command("run <id>").description("Manually trigger a schedule").option("--json", "Output results as JSON", false).action(async (id, opts) => {
|
|
4773
|
+
try {
|
|
4774
|
+
const schedule = getSchedule(id);
|
|
4775
|
+
if (!schedule) {
|
|
4776
|
+
console.error(chalk2.red(`Schedule not found: ${id}`));
|
|
4777
|
+
process.exit(1);
|
|
4778
|
+
return;
|
|
4779
|
+
}
|
|
4780
|
+
console.log(chalk2.blue(`Running schedule ${chalk2.bold(schedule.name)} against ${schedule.url}...`));
|
|
4781
|
+
const { run, results } = await runByFilter({
|
|
4782
|
+
url: schedule.url,
|
|
4783
|
+
tags: schedule.scenarioFilter.tags,
|
|
4784
|
+
priority: schedule.scenarioFilter.priority,
|
|
4785
|
+
scenarioIds: schedule.scenarioFilter.scenarioIds,
|
|
4786
|
+
model: schedule.model ?? undefined,
|
|
4787
|
+
headed: schedule.headed,
|
|
4788
|
+
parallel: schedule.parallel,
|
|
4789
|
+
timeout: schedule.timeoutMs ?? undefined,
|
|
4790
|
+
projectId: schedule.projectId ?? undefined
|
|
4791
|
+
});
|
|
4792
|
+
if (opts.json) {
|
|
4793
|
+
console.log(formatJSON(run, results));
|
|
4794
|
+
} else {
|
|
4795
|
+
console.log(formatTerminal(run, results));
|
|
4796
|
+
}
|
|
4797
|
+
process.exit(getExitCode(run));
|
|
4798
|
+
} catch (error) {
|
|
4799
|
+
console.error(chalk2.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
4800
|
+
process.exit(1);
|
|
4801
|
+
}
|
|
4802
|
+
});
|
|
4803
|
+
program2.command("daemon").description("Start the scheduler daemon").option("--interval <seconds>", "Check interval in seconds", "60").action(async (opts) => {
|
|
4804
|
+
try {
|
|
4805
|
+
const intervalMs = parseInt(opts.interval, 10) * 1000;
|
|
4806
|
+
console.log(chalk2.blue("Scheduler daemon started. Press Ctrl+C to stop."));
|
|
4807
|
+
console.log(chalk2.dim(` Check interval: ${opts.interval}s`));
|
|
4808
|
+
let running = true;
|
|
4809
|
+
const checkAndRun = async () => {
|
|
4810
|
+
while (running) {
|
|
4811
|
+
try {
|
|
4812
|
+
const schedules = listSchedules({ enabled: true });
|
|
4813
|
+
const now2 = new Date().toISOString();
|
|
4814
|
+
for (const schedule of schedules) {
|
|
4815
|
+
if (schedule.nextRunAt && schedule.nextRunAt <= now2) {
|
|
4816
|
+
console.log(chalk2.blue(`[${new Date().toISOString()}] Triggering schedule: ${schedule.name}`));
|
|
4817
|
+
try {
|
|
4818
|
+
const { run } = await runByFilter({
|
|
4819
|
+
url: schedule.url,
|
|
4820
|
+
tags: schedule.scenarioFilter.tags,
|
|
4821
|
+
priority: schedule.scenarioFilter.priority,
|
|
4822
|
+
scenarioIds: schedule.scenarioFilter.scenarioIds,
|
|
4823
|
+
model: schedule.model ?? undefined,
|
|
4824
|
+
headed: schedule.headed,
|
|
4825
|
+
parallel: schedule.parallel,
|
|
4826
|
+
timeout: schedule.timeoutMs ?? undefined,
|
|
4827
|
+
projectId: schedule.projectId ?? undefined
|
|
4828
|
+
});
|
|
4829
|
+
const statusColor = run.status === "passed" ? chalk2.green : chalk2.red;
|
|
4830
|
+
console.log(` ${statusColor(run.status)} \u2014 ${run.passed}/${run.total} passed`);
|
|
4831
|
+
updateSchedule(schedule.id, {});
|
|
4832
|
+
} catch (err) {
|
|
4833
|
+
console.error(chalk2.red(` Error running schedule ${schedule.name}: ${err instanceof Error ? err.message : String(err)}`));
|
|
4834
|
+
}
|
|
4835
|
+
}
|
|
4836
|
+
}
|
|
4837
|
+
} catch (err) {
|
|
4838
|
+
console.error(chalk2.red(`Daemon error: ${err instanceof Error ? err.message : String(err)}`));
|
|
4839
|
+
}
|
|
4840
|
+
await new Promise((resolve2) => setTimeout(resolve2, intervalMs));
|
|
4841
|
+
}
|
|
4842
|
+
};
|
|
4843
|
+
process.on("SIGINT", () => {
|
|
4844
|
+
console.log(chalk2.yellow(`
|
|
4845
|
+
Shutting down scheduler daemon...`));
|
|
4846
|
+
running = false;
|
|
4847
|
+
process.exit(0);
|
|
4848
|
+
});
|
|
4849
|
+
process.on("SIGTERM", () => {
|
|
4850
|
+
console.log(chalk2.yellow(`
|
|
4851
|
+
Shutting down scheduler daemon...`));
|
|
4852
|
+
running = false;
|
|
4853
|
+
process.exit(0);
|
|
4854
|
+
});
|
|
4855
|
+
await checkAndRun();
|
|
4856
|
+
} catch (error) {
|
|
4857
|
+
console.error(chalk2.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
4858
|
+
process.exit(1);
|
|
4859
|
+
}
|
|
4860
|
+
});
|
|
4112
4861
|
program2.parse();
|