@hasna/testers 0.0.4 → 0.0.6
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 +490 -9
- package/dist/db/database.d.ts.map +1 -1
- package/dist/db/flows.d.ts +12 -0
- package/dist/db/flows.d.ts.map +1 -0
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +295 -8
- package/dist/lib/ai-client.d.ts +9 -0
- package/dist/lib/ai-client.d.ts.map +1 -1
- package/dist/lib/runner.d.ts +6 -1
- package/dist/lib/runner.d.ts.map +1 -1
- package/dist/mcp/index.js +955 -669
- package/dist/server/index.js +380 -85
- package/dist/types/index.d.ts +31 -0
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/server/index.js
CHANGED
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
// @bun
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __export = (target, all) => {
|
|
5
|
+
for (var name in all)
|
|
6
|
+
__defProp(target, name, {
|
|
7
|
+
get: all[name],
|
|
8
|
+
enumerable: true,
|
|
9
|
+
configurable: true,
|
|
10
|
+
set: (newValue) => all[name] = () => newValue
|
|
11
|
+
});
|
|
12
|
+
};
|
|
13
|
+
var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
8
14
|
|
|
9
15
|
// src/types/index.ts
|
|
10
|
-
var MODEL_MAP = {
|
|
11
|
-
quick: "claude-haiku-4-5-20251001",
|
|
12
|
-
thorough: "claude-sonnet-4-6-20260311",
|
|
13
|
-
deep: "claude-opus-4-6-20260311"
|
|
14
|
-
};
|
|
15
16
|
function scenarioFromRow(row) {
|
|
16
17
|
return {
|
|
17
18
|
id: row.id,
|
|
@@ -103,39 +104,61 @@ function scheduleFromRow(row) {
|
|
|
103
104
|
updatedAt: row.updated_at
|
|
104
105
|
};
|
|
105
106
|
}
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
this.name = "BrowserError";
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
class AIClientError extends Error {
|
|
121
|
-
constructor(message) {
|
|
122
|
-
super(message);
|
|
123
|
-
this.name = "AIClientError";
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
class ScheduleNotFoundError extends Error {
|
|
127
|
-
constructor(id) {
|
|
128
|
-
super(`Schedule not found: ${id}`);
|
|
129
|
-
this.name = "ScheduleNotFoundError";
|
|
130
|
-
}
|
|
107
|
+
function flowFromRow(row) {
|
|
108
|
+
return {
|
|
109
|
+
id: row.id,
|
|
110
|
+
projectId: row.project_id,
|
|
111
|
+
name: row.name,
|
|
112
|
+
description: row.description,
|
|
113
|
+
scenarioIds: JSON.parse(row.scenario_ids),
|
|
114
|
+
createdAt: row.created_at,
|
|
115
|
+
updatedAt: row.updated_at
|
|
116
|
+
};
|
|
131
117
|
}
|
|
118
|
+
var MODEL_MAP, VersionConflictError, BrowserError, AIClientError, ScheduleNotFoundError, DependencyCycleError;
|
|
119
|
+
var init_types = __esm(() => {
|
|
120
|
+
MODEL_MAP = {
|
|
121
|
+
quick: "claude-haiku-4-5-20251001",
|
|
122
|
+
thorough: "claude-sonnet-4-6-20260311",
|
|
123
|
+
deep: "claude-opus-4-6-20260311"
|
|
124
|
+
};
|
|
125
|
+
VersionConflictError = class VersionConflictError extends Error {
|
|
126
|
+
constructor(entity, id) {
|
|
127
|
+
super(`Version conflict on ${entity}: ${id}`);
|
|
128
|
+
this.name = "VersionConflictError";
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
BrowserError = class BrowserError extends Error {
|
|
132
|
+
constructor(message) {
|
|
133
|
+
super(message);
|
|
134
|
+
this.name = "BrowserError";
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
AIClientError = class AIClientError extends Error {
|
|
138
|
+
constructor(message) {
|
|
139
|
+
super(message);
|
|
140
|
+
this.name = "AIClientError";
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
ScheduleNotFoundError = class ScheduleNotFoundError extends Error {
|
|
144
|
+
constructor(id) {
|
|
145
|
+
super(`Schedule not found: ${id}`);
|
|
146
|
+
this.name = "ScheduleNotFoundError";
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
DependencyCycleError = class DependencyCycleError extends Error {
|
|
150
|
+
constructor(scenarioId, dependsOn) {
|
|
151
|
+
super(`Adding dependency ${dependsOn} to ${scenarioId} would create a cycle`);
|
|
152
|
+
this.name = "DependencyCycleError";
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
});
|
|
132
156
|
|
|
133
157
|
// src/db/database.ts
|
|
134
158
|
import { Database } from "bun:sqlite";
|
|
135
159
|
import { mkdirSync, existsSync } from "fs";
|
|
136
160
|
import { dirname, join } from "path";
|
|
137
161
|
import { homedir } from "os";
|
|
138
|
-
var db = null;
|
|
139
162
|
function now() {
|
|
140
163
|
return new Date().toISOString();
|
|
141
164
|
}
|
|
@@ -154,8 +177,50 @@ function resolveDbPath() {
|
|
|
154
177
|
mkdirSync(dir, { recursive: true });
|
|
155
178
|
return join(dir, "testers.db");
|
|
156
179
|
}
|
|
157
|
-
|
|
158
|
-
|
|
180
|
+
function applyMigrations(database) {
|
|
181
|
+
const applied = database.query("SELECT id FROM _migrations ORDER BY id").all();
|
|
182
|
+
const appliedIds = new Set(applied.map((r) => r.id));
|
|
183
|
+
for (let i = 0;i < MIGRATIONS.length; i++) {
|
|
184
|
+
const migrationId = i + 1;
|
|
185
|
+
if (appliedIds.has(migrationId))
|
|
186
|
+
continue;
|
|
187
|
+
const migration = MIGRATIONS[i];
|
|
188
|
+
database.exec(migration);
|
|
189
|
+
database.query("INSERT INTO _migrations (id, applied_at) VALUES (?, ?)").run(migrationId, now());
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
function getDatabase() {
|
|
193
|
+
if (db)
|
|
194
|
+
return db;
|
|
195
|
+
const dbPath = resolveDbPath();
|
|
196
|
+
const dir = dirname(dbPath);
|
|
197
|
+
if (dbPath !== ":memory:" && !existsSync(dir)) {
|
|
198
|
+
mkdirSync(dir, { recursive: true });
|
|
199
|
+
}
|
|
200
|
+
db = new Database(dbPath);
|
|
201
|
+
db.exec("PRAGMA journal_mode = WAL");
|
|
202
|
+
db.exec("PRAGMA foreign_keys = ON");
|
|
203
|
+
db.exec("PRAGMA busy_timeout = 5000");
|
|
204
|
+
db.exec(`
|
|
205
|
+
CREATE TABLE IF NOT EXISTS _migrations (
|
|
206
|
+
id INTEGER PRIMARY KEY,
|
|
207
|
+
applied_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
208
|
+
);
|
|
209
|
+
`);
|
|
210
|
+
applyMigrations(db);
|
|
211
|
+
return db;
|
|
212
|
+
}
|
|
213
|
+
function resolvePartialId(table, partialId) {
|
|
214
|
+
const database = getDatabase();
|
|
215
|
+
const rows = database.query(`SELECT id FROM ${table} WHERE id LIKE ? || '%'`).all(partialId);
|
|
216
|
+
if (rows.length === 1)
|
|
217
|
+
return rows[0].id;
|
|
218
|
+
return null;
|
|
219
|
+
}
|
|
220
|
+
var db = null, MIGRATIONS;
|
|
221
|
+
var init_database = __esm(() => {
|
|
222
|
+
MIGRATIONS = [
|
|
223
|
+
`
|
|
159
224
|
CREATE TABLE IF NOT EXISTS projects (
|
|
160
225
|
id TEXT PRIMARY KEY,
|
|
161
226
|
name TEXT NOT NULL UNIQUE,
|
|
@@ -244,7 +309,7 @@ var MIGRATIONS = [
|
|
|
244
309
|
applied_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
245
310
|
);
|
|
246
311
|
`,
|
|
247
|
-
|
|
312
|
+
`
|
|
248
313
|
CREATE INDEX IF NOT EXISTS idx_scenarios_project ON scenarios(project_id);
|
|
249
314
|
CREATE INDEX IF NOT EXISTS idx_scenarios_priority ON scenarios(priority);
|
|
250
315
|
CREATE INDEX IF NOT EXISTS idx_scenarios_short_id ON scenarios(short_id);
|
|
@@ -255,11 +320,11 @@ var MIGRATIONS = [
|
|
|
255
320
|
CREATE INDEX IF NOT EXISTS idx_results_status ON results(status);
|
|
256
321
|
CREATE INDEX IF NOT EXISTS idx_screenshots_result ON screenshots(result_id);
|
|
257
322
|
`,
|
|
258
|
-
|
|
323
|
+
`
|
|
259
324
|
ALTER TABLE projects ADD COLUMN scenario_prefix TEXT DEFAULT 'TST';
|
|
260
325
|
ALTER TABLE projects ADD COLUMN scenario_counter INTEGER DEFAULT 0;
|
|
261
326
|
`,
|
|
262
|
-
|
|
327
|
+
`
|
|
263
328
|
CREATE TABLE IF NOT EXISTS schedules (
|
|
264
329
|
id TEXT PRIMARY KEY,
|
|
265
330
|
project_id TEXT REFERENCES projects(id) ON DELETE CASCADE,
|
|
@@ -283,12 +348,12 @@ var MIGRATIONS = [
|
|
|
283
348
|
CREATE INDEX IF NOT EXISTS idx_schedules_enabled ON schedules(enabled);
|
|
284
349
|
CREATE INDEX IF NOT EXISTS idx_schedules_next_run ON schedules(next_run_at);
|
|
285
350
|
`,
|
|
286
|
-
|
|
351
|
+
`
|
|
287
352
|
ALTER TABLE screenshots ADD COLUMN description TEXT;
|
|
288
353
|
ALTER TABLE screenshots ADD COLUMN page_url TEXT;
|
|
289
354
|
ALTER TABLE screenshots ADD COLUMN thumbnail_path TEXT;
|
|
290
355
|
`,
|
|
291
|
-
|
|
356
|
+
`
|
|
292
357
|
CREATE TABLE IF NOT EXISTS auth_presets (
|
|
293
358
|
id TEXT PRIMARY KEY,
|
|
294
359
|
name TEXT NOT NULL UNIQUE,
|
|
@@ -299,7 +364,7 @@ var MIGRATIONS = [
|
|
|
299
364
|
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
300
365
|
);
|
|
301
366
|
`,
|
|
302
|
-
|
|
367
|
+
`
|
|
303
368
|
CREATE TABLE IF NOT EXISTS webhooks (
|
|
304
369
|
id TEXT PRIMARY KEY,
|
|
305
370
|
url TEXT NOT NULL,
|
|
@@ -310,50 +375,191 @@ var MIGRATIONS = [
|
|
|
310
375
|
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
311
376
|
);
|
|
312
377
|
CREATE INDEX IF NOT EXISTS idx_webhooks_active ON webhooks(active);
|
|
378
|
+
`,
|
|
379
|
+
`
|
|
380
|
+
CREATE TABLE IF NOT EXISTS scenario_dependencies (
|
|
381
|
+
scenario_id TEXT NOT NULL REFERENCES scenarios(id) ON DELETE CASCADE,
|
|
382
|
+
depends_on TEXT NOT NULL REFERENCES scenarios(id) ON DELETE CASCADE,
|
|
383
|
+
PRIMARY KEY (scenario_id, depends_on),
|
|
384
|
+
CHECK (scenario_id != depends_on)
|
|
385
|
+
);
|
|
386
|
+
|
|
387
|
+
CREATE TABLE IF NOT EXISTS flows (
|
|
388
|
+
id TEXT PRIMARY KEY,
|
|
389
|
+
project_id TEXT REFERENCES projects(id) ON DELETE CASCADE,
|
|
390
|
+
name TEXT NOT NULL,
|
|
391
|
+
description TEXT,
|
|
392
|
+
scenario_ids TEXT NOT NULL DEFAULT '[]',
|
|
393
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
394
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
395
|
+
);
|
|
396
|
+
|
|
397
|
+
CREATE INDEX IF NOT EXISTS idx_deps_scenario ON scenario_dependencies(scenario_id);
|
|
398
|
+
CREATE INDEX IF NOT EXISTS idx_deps_depends ON scenario_dependencies(depends_on);
|
|
399
|
+
CREATE INDEX IF NOT EXISTS idx_flows_project ON flows(project_id);
|
|
313
400
|
`
|
|
314
|
-
];
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
401
|
+
];
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
// src/db/flows.ts
|
|
405
|
+
var exports_flows = {};
|
|
406
|
+
__export(exports_flows, {
|
|
407
|
+
topologicalSort: () => topologicalSort,
|
|
408
|
+
removeDependency: () => removeDependency,
|
|
409
|
+
listFlows: () => listFlows,
|
|
410
|
+
getTransitiveDependencies: () => getTransitiveDependencies,
|
|
411
|
+
getFlow: () => getFlow,
|
|
412
|
+
getDependents: () => getDependents,
|
|
413
|
+
getDependencies: () => getDependencies,
|
|
414
|
+
deleteFlow: () => deleteFlow,
|
|
415
|
+
createFlow: () => createFlow,
|
|
416
|
+
addDependency: () => addDependency
|
|
417
|
+
});
|
|
418
|
+
function addDependency(scenarioId, dependsOn) {
|
|
419
|
+
const db2 = getDatabase();
|
|
420
|
+
const visited = new Set;
|
|
421
|
+
const queue = [dependsOn];
|
|
422
|
+
while (queue.length > 0) {
|
|
423
|
+
const current = queue.shift();
|
|
424
|
+
if (current === scenarioId) {
|
|
425
|
+
throw new DependencyCycleError(scenarioId, dependsOn);
|
|
426
|
+
}
|
|
427
|
+
if (visited.has(current))
|
|
321
428
|
continue;
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
429
|
+
visited.add(current);
|
|
430
|
+
const deps = db2.query("SELECT depends_on FROM scenario_dependencies WHERE scenario_id = ?").all(current);
|
|
431
|
+
for (const dep of deps) {
|
|
432
|
+
if (!visited.has(dep.depends_on)) {
|
|
433
|
+
queue.push(dep.depends_on);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
325
436
|
}
|
|
437
|
+
db2.query("INSERT OR IGNORE INTO scenario_dependencies (scenario_id, depends_on) VALUES (?, ?)").run(scenarioId, dependsOn);
|
|
326
438
|
}
|
|
327
|
-
function
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
439
|
+
function removeDependency(scenarioId, dependsOn) {
|
|
440
|
+
const db2 = getDatabase();
|
|
441
|
+
const result = db2.query("DELETE FROM scenario_dependencies WHERE scenario_id = ? AND depends_on = ?").run(scenarioId, dependsOn);
|
|
442
|
+
return result.changes > 0;
|
|
443
|
+
}
|
|
444
|
+
function getDependencies(scenarioId) {
|
|
445
|
+
const db2 = getDatabase();
|
|
446
|
+
const rows = db2.query("SELECT depends_on FROM scenario_dependencies WHERE scenario_id = ?").all(scenarioId);
|
|
447
|
+
return rows.map((r) => r.depends_on);
|
|
448
|
+
}
|
|
449
|
+
function getDependents(scenarioId) {
|
|
450
|
+
const db2 = getDatabase();
|
|
451
|
+
const rows = db2.query("SELECT scenario_id FROM scenario_dependencies WHERE depends_on = ?").all(scenarioId);
|
|
452
|
+
return rows.map((r) => r.scenario_id);
|
|
453
|
+
}
|
|
454
|
+
function getTransitiveDependencies(scenarioId) {
|
|
455
|
+
const db2 = getDatabase();
|
|
456
|
+
const visited = new Set;
|
|
457
|
+
const queue = [scenarioId];
|
|
458
|
+
while (queue.length > 0) {
|
|
459
|
+
const current = queue.shift();
|
|
460
|
+
const deps = db2.query("SELECT depends_on FROM scenario_dependencies WHERE scenario_id = ?").all(current);
|
|
461
|
+
for (const dep of deps) {
|
|
462
|
+
if (!visited.has(dep.depends_on)) {
|
|
463
|
+
visited.add(dep.depends_on);
|
|
464
|
+
queue.push(dep.depends_on);
|
|
465
|
+
}
|
|
466
|
+
}
|
|
334
467
|
}
|
|
335
|
-
|
|
336
|
-
db.exec("PRAGMA journal_mode = WAL");
|
|
337
|
-
db.exec("PRAGMA foreign_keys = ON");
|
|
338
|
-
db.exec("PRAGMA busy_timeout = 5000");
|
|
339
|
-
db.exec(`
|
|
340
|
-
CREATE TABLE IF NOT EXISTS _migrations (
|
|
341
|
-
id INTEGER PRIMARY KEY,
|
|
342
|
-
applied_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
343
|
-
);
|
|
344
|
-
`);
|
|
345
|
-
applyMigrations(db);
|
|
346
|
-
return db;
|
|
468
|
+
return Array.from(visited);
|
|
347
469
|
}
|
|
348
|
-
function
|
|
349
|
-
const
|
|
350
|
-
const
|
|
351
|
-
|
|
352
|
-
|
|
470
|
+
function topologicalSort(scenarioIds) {
|
|
471
|
+
const db2 = getDatabase();
|
|
472
|
+
const idSet = new Set(scenarioIds);
|
|
473
|
+
const inDegree = new Map;
|
|
474
|
+
const dependents = new Map;
|
|
475
|
+
for (const id of scenarioIds) {
|
|
476
|
+
inDegree.set(id, 0);
|
|
477
|
+
dependents.set(id, []);
|
|
478
|
+
}
|
|
479
|
+
for (const id of scenarioIds) {
|
|
480
|
+
const deps = db2.query("SELECT depends_on FROM scenario_dependencies WHERE scenario_id = ?").all(id);
|
|
481
|
+
for (const dep of deps) {
|
|
482
|
+
if (idSet.has(dep.depends_on)) {
|
|
483
|
+
inDegree.set(id, (inDegree.get(id) ?? 0) + 1);
|
|
484
|
+
dependents.get(dep.depends_on).push(id);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
const queue = [];
|
|
489
|
+
for (const [id, deg] of inDegree) {
|
|
490
|
+
if (deg === 0)
|
|
491
|
+
queue.push(id);
|
|
492
|
+
}
|
|
493
|
+
const sorted = [];
|
|
494
|
+
while (queue.length > 0) {
|
|
495
|
+
const current = queue.shift();
|
|
496
|
+
sorted.push(current);
|
|
497
|
+
for (const dep of dependents.get(current) ?? []) {
|
|
498
|
+
const newDeg = (inDegree.get(dep) ?? 1) - 1;
|
|
499
|
+
inDegree.set(dep, newDeg);
|
|
500
|
+
if (newDeg === 0)
|
|
501
|
+
queue.push(dep);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
if (sorted.length !== scenarioIds.length) {
|
|
505
|
+
throw new DependencyCycleError("multiple", "multiple");
|
|
506
|
+
}
|
|
507
|
+
return sorted;
|
|
508
|
+
}
|
|
509
|
+
function createFlow(input) {
|
|
510
|
+
const db2 = getDatabase();
|
|
511
|
+
const id = uuid();
|
|
512
|
+
const timestamp = now();
|
|
513
|
+
db2.query(`
|
|
514
|
+
INSERT INTO flows (id, project_id, name, description, scenario_ids, created_at, updated_at)
|
|
515
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
516
|
+
`).run(id, input.projectId ?? null, input.name, input.description ?? null, JSON.stringify(input.scenarioIds), timestamp, timestamp);
|
|
517
|
+
return getFlow(id);
|
|
518
|
+
}
|
|
519
|
+
function getFlow(id) {
|
|
520
|
+
const db2 = getDatabase();
|
|
521
|
+
let row = db2.query("SELECT * FROM flows WHERE id = ?").get(id);
|
|
522
|
+
if (row)
|
|
523
|
+
return flowFromRow(row);
|
|
524
|
+
const fullId = resolvePartialId("flows", id);
|
|
525
|
+
if (fullId) {
|
|
526
|
+
row = db2.query("SELECT * FROM flows WHERE id = ?").get(fullId);
|
|
527
|
+
if (row)
|
|
528
|
+
return flowFromRow(row);
|
|
529
|
+
}
|
|
353
530
|
return null;
|
|
354
531
|
}
|
|
532
|
+
function listFlows(projectId) {
|
|
533
|
+
const db2 = getDatabase();
|
|
534
|
+
if (projectId) {
|
|
535
|
+
const rows2 = db2.query("SELECT * FROM flows WHERE project_id = ? ORDER BY created_at DESC").all(projectId);
|
|
536
|
+
return rows2.map(flowFromRow);
|
|
537
|
+
}
|
|
538
|
+
const rows = db2.query("SELECT * FROM flows ORDER BY created_at DESC").all();
|
|
539
|
+
return rows.map(flowFromRow);
|
|
540
|
+
}
|
|
541
|
+
function deleteFlow(id) {
|
|
542
|
+
const db2 = getDatabase();
|
|
543
|
+
const flow = getFlow(id);
|
|
544
|
+
if (!flow)
|
|
545
|
+
return false;
|
|
546
|
+
const result = db2.query("DELETE FROM flows WHERE id = ?").run(flow.id);
|
|
547
|
+
return result.changes > 0;
|
|
548
|
+
}
|
|
549
|
+
var init_flows = __esm(() => {
|
|
550
|
+
init_database();
|
|
551
|
+
init_database();
|
|
552
|
+
init_types();
|
|
553
|
+
});
|
|
554
|
+
|
|
555
|
+
// src/server/index.ts
|
|
556
|
+
import { existsSync as existsSync4 } from "fs";
|
|
557
|
+
import { join as join4 } from "path";
|
|
558
|
+
import { homedir as homedir4 } from "os";
|
|
355
559
|
|
|
356
560
|
// src/db/scenarios.ts
|
|
561
|
+
init_types();
|
|
562
|
+
init_database();
|
|
357
563
|
function nextShortId(projectId) {
|
|
358
564
|
const db2 = getDatabase();
|
|
359
565
|
if (projectId) {
|
|
@@ -517,6 +723,8 @@ function deleteScenario(id) {
|
|
|
517
723
|
}
|
|
518
724
|
|
|
519
725
|
// src/db/runs.ts
|
|
726
|
+
init_types();
|
|
727
|
+
init_database();
|
|
520
728
|
function createRun(input) {
|
|
521
729
|
const db2 = getDatabase();
|
|
522
730
|
const id = uuid();
|
|
@@ -629,6 +837,8 @@ function updateRun(id, updates) {
|
|
|
629
837
|
}
|
|
630
838
|
|
|
631
839
|
// src/db/results.ts
|
|
840
|
+
init_types();
|
|
841
|
+
init_database();
|
|
632
842
|
function createResult(input) {
|
|
633
843
|
const db2 = getDatabase();
|
|
634
844
|
const id = uuid();
|
|
@@ -705,6 +915,8 @@ function getResultsByRun(runId) {
|
|
|
705
915
|
}
|
|
706
916
|
|
|
707
917
|
// src/db/screenshots.ts
|
|
918
|
+
init_types();
|
|
919
|
+
init_database();
|
|
708
920
|
function createScreenshot(input) {
|
|
709
921
|
const db2 = getDatabase();
|
|
710
922
|
const id = uuid();
|
|
@@ -728,6 +940,7 @@ function listScreenshots(resultId) {
|
|
|
728
940
|
|
|
729
941
|
// src/lib/browser.ts
|
|
730
942
|
import { chromium } from "playwright";
|
|
943
|
+
init_types();
|
|
731
944
|
var DEFAULT_VIEWPORT = { width: 1280, height: 720 };
|
|
732
945
|
async function launchBrowser(options) {
|
|
733
946
|
const headless = options?.headless ?? true;
|
|
@@ -947,6 +1160,7 @@ class Screenshotter {
|
|
|
947
1160
|
}
|
|
948
1161
|
|
|
949
1162
|
// src/lib/ai-client.ts
|
|
1163
|
+
init_types();
|
|
950
1164
|
import Anthropic from "@anthropic-ai/sdk";
|
|
951
1165
|
function resolveModel(nameOrPreset) {
|
|
952
1166
|
if (nameOrPreset in MODEL_MAP) {
|
|
@@ -1499,7 +1713,8 @@ async function runAgentLoop(options) {
|
|
|
1499
1713
|
screenshotter,
|
|
1500
1714
|
model,
|
|
1501
1715
|
runId,
|
|
1502
|
-
maxTurns = 30
|
|
1716
|
+
maxTurns = 30,
|
|
1717
|
+
onStep
|
|
1503
1718
|
} = options;
|
|
1504
1719
|
const systemPrompt = [
|
|
1505
1720
|
"You are an expert QA testing agent. Your job is to thoroughly test web application scenarios.",
|
|
@@ -1558,8 +1773,8 @@ async function runAgentLoop(options) {
|
|
|
1558
1773
|
}
|
|
1559
1774
|
const toolUseBlocks = response.content.filter((block) => block.type === "tool_use");
|
|
1560
1775
|
if (toolUseBlocks.length === 0 && response.stop_reason === "end_turn") {
|
|
1561
|
-
const
|
|
1562
|
-
const textReasoning =
|
|
1776
|
+
const textBlocks2 = response.content.filter((block) => block.type === "text");
|
|
1777
|
+
const textReasoning = textBlocks2.map((b) => b.text).join(`
|
|
1563
1778
|
`);
|
|
1564
1779
|
return {
|
|
1565
1780
|
status: "error",
|
|
@@ -1570,10 +1785,22 @@ async function runAgentLoop(options) {
|
|
|
1570
1785
|
};
|
|
1571
1786
|
}
|
|
1572
1787
|
const toolResults = [];
|
|
1788
|
+
const textBlocks = response.content.filter((block) => block.type === "text");
|
|
1789
|
+
if (textBlocks.length > 0 && onStep) {
|
|
1790
|
+
const thinking = textBlocks.map((b) => b.text).join(`
|
|
1791
|
+
`);
|
|
1792
|
+
onStep({ type: "thinking", thinking, stepNumber });
|
|
1793
|
+
}
|
|
1573
1794
|
for (const toolBlock of toolUseBlocks) {
|
|
1574
1795
|
stepNumber++;
|
|
1575
1796
|
const toolInput = toolBlock.input;
|
|
1797
|
+
if (onStep) {
|
|
1798
|
+
onStep({ type: "tool_call", toolName: toolBlock.name, toolInput, stepNumber });
|
|
1799
|
+
}
|
|
1576
1800
|
const execResult = await executeTool(page, screenshotter, toolBlock.name, toolInput, { runId, scenarioSlug, stepNumber });
|
|
1801
|
+
if (onStep) {
|
|
1802
|
+
onStep({ type: "tool_result", toolName: toolBlock.name, toolResult: execResult.result, stepNumber });
|
|
1803
|
+
}
|
|
1577
1804
|
if (execResult.screenshot) {
|
|
1578
1805
|
screenshots.push({
|
|
1579
1806
|
...execResult.screenshot,
|
|
@@ -1625,6 +1852,7 @@ function createClient(apiKey) {
|
|
|
1625
1852
|
}
|
|
1626
1853
|
|
|
1627
1854
|
// src/lib/config.ts
|
|
1855
|
+
init_types();
|
|
1628
1856
|
import { homedir as homedir3 } from "os";
|
|
1629
1857
|
import { join as join3 } from "path";
|
|
1630
1858
|
import { readFileSync, existsSync as existsSync3 } from "fs";
|
|
@@ -1715,7 +1943,20 @@ async function runSingleScenario(scenario, runId, options) {
|
|
|
1715
1943
|
screenshotter,
|
|
1716
1944
|
model,
|
|
1717
1945
|
runId,
|
|
1718
|
-
maxTurns: 30
|
|
1946
|
+
maxTurns: 30,
|
|
1947
|
+
onStep: (stepEvent) => {
|
|
1948
|
+
emit({
|
|
1949
|
+
type: `step:${stepEvent.type}`,
|
|
1950
|
+
scenarioId: scenario.id,
|
|
1951
|
+
scenarioName: scenario.name,
|
|
1952
|
+
runId,
|
|
1953
|
+
toolName: stepEvent.toolName,
|
|
1954
|
+
toolInput: stepEvent.toolInput,
|
|
1955
|
+
toolResult: stepEvent.toolResult,
|
|
1956
|
+
thinking: stepEvent.thinking,
|
|
1957
|
+
stepNumber: stepEvent.stepNumber
|
|
1958
|
+
});
|
|
1959
|
+
}
|
|
1719
1960
|
});
|
|
1720
1961
|
for (const ss of agentResult.screenshots) {
|
|
1721
1962
|
createScreenshot({
|
|
@@ -1768,24 +2009,70 @@ async function runBatch(scenarios, options) {
|
|
|
1768
2009
|
projectId: options.projectId
|
|
1769
2010
|
});
|
|
1770
2011
|
updateRun(run.id, { status: "running", total: scenarios.length });
|
|
2012
|
+
let sortedScenarios = scenarios;
|
|
2013
|
+
try {
|
|
2014
|
+
const { topologicalSort: topologicalSort2 } = await Promise.resolve().then(() => (init_flows(), exports_flows));
|
|
2015
|
+
const scenarioIds = scenarios.map((s) => s.id);
|
|
2016
|
+
const sortedIds = topologicalSort2(scenarioIds);
|
|
2017
|
+
const scenarioMap = new Map(scenarios.map((s) => [s.id, s]));
|
|
2018
|
+
sortedScenarios = sortedIds.map((id) => scenarioMap.get(id)).filter((s) => s !== undefined);
|
|
2019
|
+
for (const s of scenarios) {
|
|
2020
|
+
if (!sortedIds.includes(s.id))
|
|
2021
|
+
sortedScenarios.push(s);
|
|
2022
|
+
}
|
|
2023
|
+
} catch {}
|
|
1771
2024
|
const results = [];
|
|
2025
|
+
const failedScenarioIds = new Set;
|
|
2026
|
+
const canRun = async (scenario) => {
|
|
2027
|
+
try {
|
|
2028
|
+
const { getDependencies: getDependencies2 } = await Promise.resolve().then(() => (init_flows(), exports_flows));
|
|
2029
|
+
const deps = getDependencies2(scenario.id);
|
|
2030
|
+
for (const depId of deps) {
|
|
2031
|
+
if (failedScenarioIds.has(depId))
|
|
2032
|
+
return false;
|
|
2033
|
+
}
|
|
2034
|
+
} catch {}
|
|
2035
|
+
return true;
|
|
2036
|
+
};
|
|
1772
2037
|
if (parallel <= 1) {
|
|
1773
|
-
for (const scenario of
|
|
2038
|
+
for (const scenario of sortedScenarios) {
|
|
2039
|
+
if (!await canRun(scenario)) {
|
|
2040
|
+
const result2 = createResult({ runId: run.id, scenarioId: scenario.id, model, stepsTotal: 0 });
|
|
2041
|
+
const skipped = updateResult(result2.id, { status: "skipped", error: "Skipped: dependency failed" });
|
|
2042
|
+
results.push(skipped);
|
|
2043
|
+
failedScenarioIds.add(scenario.id);
|
|
2044
|
+
emit({ type: "scenario:error", scenarioId: scenario.id, scenarioName: scenario.name, error: "Dependency failed \u2014 skipped", runId: run.id });
|
|
2045
|
+
continue;
|
|
2046
|
+
}
|
|
1774
2047
|
const result = await runSingleScenario(scenario, run.id, options);
|
|
1775
2048
|
results.push(result);
|
|
2049
|
+
if (result.status === "failed" || result.status === "error") {
|
|
2050
|
+
failedScenarioIds.add(scenario.id);
|
|
2051
|
+
}
|
|
1776
2052
|
}
|
|
1777
2053
|
} else {
|
|
1778
|
-
const queue = [...
|
|
2054
|
+
const queue = [...sortedScenarios];
|
|
1779
2055
|
const running = [];
|
|
1780
2056
|
const processNext = async () => {
|
|
1781
2057
|
const scenario = queue.shift();
|
|
1782
2058
|
if (!scenario)
|
|
1783
2059
|
return;
|
|
2060
|
+
if (!await canRun(scenario)) {
|
|
2061
|
+
const result2 = createResult({ runId: run.id, scenarioId: scenario.id, model, stepsTotal: 0 });
|
|
2062
|
+
const skipped = updateResult(result2.id, { status: "skipped", error: "Skipped: dependency failed" });
|
|
2063
|
+
results.push(skipped);
|
|
2064
|
+
failedScenarioIds.add(scenario.id);
|
|
2065
|
+
await processNext();
|
|
2066
|
+
return;
|
|
2067
|
+
}
|
|
1784
2068
|
const result = await runSingleScenario(scenario, run.id, options);
|
|
1785
2069
|
results.push(result);
|
|
2070
|
+
if (result.status === "failed" || result.status === "error") {
|
|
2071
|
+
failedScenarioIds.add(scenario.id);
|
|
2072
|
+
}
|
|
1786
2073
|
await processNext();
|
|
1787
2074
|
};
|
|
1788
|
-
const workers = Math.min(parallel,
|
|
2075
|
+
const workers = Math.min(parallel, sortedScenarios.length);
|
|
1789
2076
|
for (let i = 0;i < workers; i++) {
|
|
1790
2077
|
running.push(processNext());
|
|
1791
2078
|
}
|
|
@@ -1835,7 +2122,14 @@ function estimateCost(model, tokens) {
|
|
|
1835
2122
|
return tokens / 1e6 * costPer1M * 100;
|
|
1836
2123
|
}
|
|
1837
2124
|
|
|
2125
|
+
// src/server/index.ts
|
|
2126
|
+
init_types();
|
|
2127
|
+
init_database();
|
|
2128
|
+
|
|
1838
2129
|
// src/db/schedules.ts
|
|
2130
|
+
init_database();
|
|
2131
|
+
init_types();
|
|
2132
|
+
init_database();
|
|
1839
2133
|
function createSchedule(input) {
|
|
1840
2134
|
const db2 = getDatabase();
|
|
1841
2135
|
const id = uuid();
|
|
@@ -1962,6 +2256,7 @@ function updateLastRun(id, runId, nextRunAt) {
|
|
|
1962
2256
|
}
|
|
1963
2257
|
|
|
1964
2258
|
// src/lib/scheduler.ts
|
|
2259
|
+
init_types();
|
|
1965
2260
|
function parseCronField(field, min, max) {
|
|
1966
2261
|
const results = new Set;
|
|
1967
2262
|
const parts = field.split(",");
|
package/dist/types/index.d.ts
CHANGED
|
@@ -359,4 +359,35 @@ export declare class AgentNotFoundError extends Error {
|
|
|
359
359
|
export declare class ScheduleNotFoundError extends Error {
|
|
360
360
|
constructor(id: string);
|
|
361
361
|
}
|
|
362
|
+
export declare class FlowNotFoundError extends Error {
|
|
363
|
+
constructor(id: string);
|
|
364
|
+
}
|
|
365
|
+
export declare class DependencyCycleError extends Error {
|
|
366
|
+
constructor(scenarioId: string, dependsOn: string);
|
|
367
|
+
}
|
|
368
|
+
export interface FlowRow {
|
|
369
|
+
id: string;
|
|
370
|
+
project_id: string | null;
|
|
371
|
+
name: string;
|
|
372
|
+
description: string | null;
|
|
373
|
+
scenario_ids: string;
|
|
374
|
+
created_at: string;
|
|
375
|
+
updated_at: string;
|
|
376
|
+
}
|
|
377
|
+
export interface Flow {
|
|
378
|
+
id: string;
|
|
379
|
+
projectId: string | null;
|
|
380
|
+
name: string;
|
|
381
|
+
description: string | null;
|
|
382
|
+
scenarioIds: string[];
|
|
383
|
+
createdAt: string;
|
|
384
|
+
updatedAt: string;
|
|
385
|
+
}
|
|
386
|
+
export declare function flowFromRow(row: FlowRow): Flow;
|
|
387
|
+
export interface CreateFlowInput {
|
|
388
|
+
name: string;
|
|
389
|
+
description?: string;
|
|
390
|
+
scenarioIds: string[];
|
|
391
|
+
projectId?: string;
|
|
392
|
+
}
|
|
362
393
|
//# sourceMappingURL=index.d.ts.map
|