@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.
@@ -1,17 +1,18 @@
1
1
  #!/usr/bin/env bun
2
2
  // @bun
3
-
4
- // src/server/index.ts
5
- import { existsSync as existsSync4 } from "fs";
6
- import { join as join4 } from "path";
7
- import { homedir as homedir4 } from "os";
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
- class VersionConflictError extends Error {
107
- constructor(entity, id) {
108
- super(`Version conflict on ${entity}: ${id}`);
109
- this.name = "VersionConflictError";
110
- }
111
- }
112
-
113
- class BrowserError extends Error {
114
- constructor(message) {
115
- super(message);
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
- var MIGRATIONS = [
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
- function applyMigrations(database) {
316
- const applied = database.query("SELECT id FROM _migrations ORDER BY id").all();
317
- const appliedIds = new Set(applied.map((r) => r.id));
318
- for (let i = 0;i < MIGRATIONS.length; i++) {
319
- const migrationId = i + 1;
320
- if (appliedIds.has(migrationId))
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
- const migration = MIGRATIONS[i];
323
- database.exec(migration);
324
- database.query("INSERT INTO _migrations (id, applied_at) VALUES (?, ?)").run(migrationId, now());
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 getDatabase() {
328
- if (db)
329
- return db;
330
- const dbPath = resolveDbPath();
331
- const dir = dirname(dbPath);
332
- if (dbPath !== ":memory:" && !existsSync(dir)) {
333
- mkdirSync(dir, { recursive: true });
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
- db = new Database(dbPath);
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 resolvePartialId(table, partialId) {
349
- const database = getDatabase();
350
- const rows = database.query(`SELECT id FROM ${table} WHERE id LIKE ? || '%'`).all(partialId);
351
- if (rows.length === 1)
352
- return rows[0].id;
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 textBlocks = response.content.filter((block) => block.type === "text");
1562
- const textReasoning = textBlocks.map((b) => b.text).join(`
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 scenarios) {
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 = [...scenarios];
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, scenarios.length);
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(",");
@@ -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