@hasna/testers 0.0.34 → 0.0.36

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/index.js CHANGED
@@ -17,6 +17,72 @@ var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
17
17
  var __require = import.meta.require;
18
18
 
19
19
  // src/types/index.ts
20
+ function isRecord(value) {
21
+ return typeof value === "object" && value !== null && !Array.isArray(value);
22
+ }
23
+ function stringValue(value) {
24
+ return typeof value === "string" && value.trim() ? value : undefined;
25
+ }
26
+ function numberValue(value) {
27
+ return typeof value === "number" && Number.isFinite(value) ? value : undefined;
28
+ }
29
+ function stringMap(value) {
30
+ if (!isRecord(value))
31
+ return;
32
+ const entries = Object.entries(value).filter((entry) => typeof entry[1] === "string");
33
+ return entries.length > 0 ? Object.fromEntries(entries) : undefined;
34
+ }
35
+ function cleanupValue(value) {
36
+ if (value === "delete" || value === "stop" || value === "keep")
37
+ return value;
38
+ return;
39
+ }
40
+ function workflowExecutionFromValue(value) {
41
+ const input = isRecord(value) ? value : {};
42
+ const rawTarget = stringValue(input["target"]) ?? "local";
43
+ if (rawTarget === "local") {
44
+ const timeoutMs2 = numberValue(input["timeoutMs"]);
45
+ return timeoutMs2 === undefined ? { target: "local" } : { target: "local", timeoutMs: timeoutMs2 };
46
+ }
47
+ if (rawTarget !== "sandbox" && rawTarget !== "connector:e2b") {
48
+ throw new Error(`Unsupported workflow execution target: ${rawTarget}`);
49
+ }
50
+ const provider = rawTarget === "connector:e2b" ? "e2b" : stringValue(input["provider"]) ?? stringValue(input["connector"]);
51
+ const sandboxImage = stringValue(input["sandboxImage"]) ?? stringValue(input["sandboxTemplate"]);
52
+ const sandboxRemoteDir = stringValue(input["sandboxRemoteDir"]);
53
+ const sandboxCleanup = cleanupValue(input["sandboxCleanup"]);
54
+ const setupCommand = stringValue(input["setupCommand"]);
55
+ const packageSpec = stringValue(input["packageSpec"]);
56
+ const timeoutMs = numberValue(input["timeoutMs"]);
57
+ const env = stringMap(input["env"]);
58
+ return {
59
+ target: "sandbox",
60
+ ...provider ? { provider } : {},
61
+ ...sandboxImage ? { sandboxImage } : {},
62
+ ...sandboxRemoteDir ? { sandboxRemoteDir } : {},
63
+ ...sandboxCleanup ? { sandboxCleanup } : {},
64
+ ...setupCommand ? { setupCommand } : {},
65
+ ...packageSpec ? { packageSpec } : {},
66
+ ...timeoutMs !== undefined ? { timeoutMs } : {},
67
+ ...env ? { env } : {}
68
+ };
69
+ }
70
+ function workflowFromRow(row) {
71
+ return {
72
+ id: row.id,
73
+ projectId: row.project_id,
74
+ name: row.name,
75
+ description: row.description,
76
+ scenarioFilter: JSON.parse(row.scenario_filter || "{}"),
77
+ personaIds: JSON.parse(row.persona_ids || "[]"),
78
+ goal: row.goal ? JSON.parse(row.goal) : null,
79
+ execution: workflowExecutionFromValue(JSON.parse(row.execution || '{"target":"local"}')),
80
+ settings: JSON.parse(row.settings || "{}"),
81
+ enabled: row.enabled === 1,
82
+ createdAt: row.created_at,
83
+ updatedAt: row.updated_at
84
+ };
85
+ }
20
86
  function projectFromRow(row) {
21
87
  return {
22
88
  id: row.id,
@@ -10456,7 +10522,7 @@ __export(exports_flows, {
10456
10522
  addDependency: () => addDependency
10457
10523
  });
10458
10524
  function addDependency(scenarioId, dependsOn) {
10459
- const db2 = getDatabase();
10525
+ const db3 = getDatabase();
10460
10526
  const visited = new Set;
10461
10527
  const queue = [dependsOn];
10462
10528
  while (queue.length > 0) {
@@ -10467,37 +10533,37 @@ function addDependency(scenarioId, dependsOn) {
10467
10533
  if (visited.has(current))
10468
10534
  continue;
10469
10535
  visited.add(current);
10470
- const deps = db2.query("SELECT depends_on FROM scenario_dependencies WHERE scenario_id = ?").all(current);
10536
+ const deps = db3.query("SELECT depends_on FROM scenario_dependencies WHERE scenario_id = ?").all(current);
10471
10537
  for (const dep of deps) {
10472
10538
  if (!visited.has(dep.depends_on)) {
10473
10539
  queue.push(dep.depends_on);
10474
10540
  }
10475
10541
  }
10476
10542
  }
10477
- db2.query("INSERT OR IGNORE INTO scenario_dependencies (scenario_id, depends_on) VALUES (?, ?)").run(scenarioId, dependsOn);
10543
+ db3.query("INSERT OR IGNORE INTO scenario_dependencies (scenario_id, depends_on) VALUES (?, ?)").run(scenarioId, dependsOn);
10478
10544
  }
10479
10545
  function removeDependency(scenarioId, dependsOn) {
10480
- const db2 = getDatabase();
10481
- const result = db2.query("DELETE FROM scenario_dependencies WHERE scenario_id = ? AND depends_on = ?").run(scenarioId, dependsOn);
10546
+ const db3 = getDatabase();
10547
+ const result = db3.query("DELETE FROM scenario_dependencies WHERE scenario_id = ? AND depends_on = ?").run(scenarioId, dependsOn);
10482
10548
  return result.changes > 0;
10483
10549
  }
10484
10550
  function getDependencies(scenarioId) {
10485
- const db2 = getDatabase();
10486
- const rows = db2.query("SELECT depends_on FROM scenario_dependencies WHERE scenario_id = ?").all(scenarioId);
10551
+ const db3 = getDatabase();
10552
+ const rows = db3.query("SELECT depends_on FROM scenario_dependencies WHERE scenario_id = ?").all(scenarioId);
10487
10553
  return rows.map((r) => r.depends_on);
10488
10554
  }
10489
10555
  function getDependents(scenarioId) {
10490
- const db2 = getDatabase();
10491
- const rows = db2.query("SELECT scenario_id FROM scenario_dependencies WHERE depends_on = ?").all(scenarioId);
10556
+ const db3 = getDatabase();
10557
+ const rows = db3.query("SELECT scenario_id FROM scenario_dependencies WHERE depends_on = ?").all(scenarioId);
10492
10558
  return rows.map((r) => r.scenario_id);
10493
10559
  }
10494
10560
  function getTransitiveDependencies(scenarioId) {
10495
- const db2 = getDatabase();
10561
+ const db3 = getDatabase();
10496
10562
  const visited = new Set;
10497
10563
  const queue = [scenarioId];
10498
10564
  while (queue.length > 0) {
10499
10565
  const current = queue.shift();
10500
- const deps = db2.query("SELECT depends_on FROM scenario_dependencies WHERE scenario_id = ?").all(current);
10566
+ const deps = db3.query("SELECT depends_on FROM scenario_dependencies WHERE scenario_id = ?").all(current);
10501
10567
  for (const dep of deps) {
10502
10568
  if (!visited.has(dep.depends_on)) {
10503
10569
  visited.add(dep.depends_on);
@@ -10508,7 +10574,7 @@ function getTransitiveDependencies(scenarioId) {
10508
10574
  return Array.from(visited);
10509
10575
  }
10510
10576
  function topologicalSort(scenarioIds) {
10511
- const db2 = getDatabase();
10577
+ const db3 = getDatabase();
10512
10578
  const idSet = new Set(scenarioIds);
10513
10579
  const inDegree = new Map;
10514
10580
  const dependents = new Map;
@@ -10517,7 +10583,7 @@ function topologicalSort(scenarioIds) {
10517
10583
  dependents.set(id, []);
10518
10584
  }
10519
10585
  for (const id of scenarioIds) {
10520
- const deps = db2.query("SELECT depends_on FROM scenario_dependencies WHERE scenario_id = ?").all(id);
10586
+ const deps = db3.query("SELECT depends_on FROM scenario_dependencies WHERE scenario_id = ?").all(id);
10521
10587
  for (const dep of deps) {
10522
10588
  if (idSet.has(dep.depends_on)) {
10523
10589
  inDegree.set(id, (inDegree.get(id) ?? 0) + 1);
@@ -10547,43 +10613,43 @@ function topologicalSort(scenarioIds) {
10547
10613
  return sorted;
10548
10614
  }
10549
10615
  function createFlow(input) {
10550
- const db2 = getDatabase();
10616
+ const db3 = getDatabase();
10551
10617
  const id = uuid();
10552
10618
  const timestamp = now();
10553
- db2.query(`
10619
+ db3.query(`
10554
10620
  INSERT INTO flows (id, project_id, name, description, scenario_ids, created_at, updated_at)
10555
10621
  VALUES (?, ?, ?, ?, ?, ?, ?)
10556
10622
  `).run(id, input.projectId ?? null, input.name, input.description ?? null, JSON.stringify(input.scenarioIds), timestamp, timestamp);
10557
10623
  return getFlow(id);
10558
10624
  }
10559
10625
  function getFlow(id) {
10560
- const db2 = getDatabase();
10561
- let row = db2.query("SELECT * FROM flows WHERE id = ?").get(id);
10626
+ const db3 = getDatabase();
10627
+ let row = db3.query("SELECT * FROM flows WHERE id = ?").get(id);
10562
10628
  if (row)
10563
10629
  return flowFromRow(row);
10564
10630
  const fullId = resolvePartialId("flows", id);
10565
10631
  if (fullId) {
10566
- row = db2.query("SELECT * FROM flows WHERE id = ?").get(fullId);
10632
+ row = db3.query("SELECT * FROM flows WHERE id = ?").get(fullId);
10567
10633
  if (row)
10568
10634
  return flowFromRow(row);
10569
10635
  }
10570
10636
  return null;
10571
10637
  }
10572
10638
  function listFlows(projectId) {
10573
- const db2 = getDatabase();
10639
+ const db3 = getDatabase();
10574
10640
  if (projectId) {
10575
- const rows2 = db2.query("SELECT * FROM flows WHERE project_id = ? ORDER BY created_at DESC").all(projectId);
10641
+ const rows2 = db3.query("SELECT * FROM flows WHERE project_id = ? ORDER BY created_at DESC").all(projectId);
10576
10642
  return rows2.map(flowFromRow);
10577
10643
  }
10578
- const rows = db2.query("SELECT * FROM flows ORDER BY created_at DESC").all();
10644
+ const rows = db3.query("SELECT * FROM flows ORDER BY created_at DESC").all();
10579
10645
  return rows.map(flowFromRow);
10580
10646
  }
10581
10647
  function deleteFlow(id) {
10582
- const db2 = getDatabase();
10648
+ const db3 = getDatabase();
10583
10649
  const flow = getFlow(id);
10584
10650
  if (!flow)
10585
10651
  return false;
10586
- const result = db2.query("DELETE FROM flows WHERE id = ?").run(flow.id);
10652
+ const result = db3.query("DELETE FROM flows WHERE id = ?").run(flow.id);
10587
10653
  return result.changes > 0;
10588
10654
  }
10589
10655
  var init_flows = __esm(() => {
@@ -10669,6 +10735,10 @@ function loadConfig() {
10669
10735
  if (envApiKey) {
10670
10736
  config.anthropicApiKey = envApiKey;
10671
10737
  }
10738
+ const envSelfHeal = process.env["TESTERS_SELF_HEAL"];
10739
+ if (envSelfHeal !== undefined) {
10740
+ config.selfHeal = ["1", "true", "yes", "on"].includes(envSelfHeal.toLowerCase());
10741
+ }
10672
10742
  return config;
10673
10743
  }
10674
10744
  function resolveModel(nameOrId) {
@@ -11586,12 +11656,11 @@ Original selector that failed: "${request.failedSelector}"
11586
11656
  Please identify the correct selector from the screenshot.`;
11587
11657
  let rawResponse = "";
11588
11658
  try {
11589
- if (provider === "openai" || provider === "google") {
11590
- const baseUrl = provider === "openai" ? "https://api.openai.com/v1" : "https://generativelanguage.googleapis.com/v1beta/openai";
11591
- const apiKey = provider === "openai" ? process.env["OPENAI_API_KEY"] ?? "" : process.env["GOOGLE_API_KEY"] ?? "";
11659
+ if (provider !== "anthropic") {
11660
+ const compat = createOpenAICompatibleConfig(provider);
11592
11661
  const resp = await callOpenAICompatible({
11593
- baseUrl,
11594
- apiKey,
11662
+ baseUrl: compat.baseUrl,
11663
+ apiKey: compat.apiKey,
11595
11664
  model,
11596
11665
  system: HEAL_SYSTEM,
11597
11666
  messages: [{ role: "user", content: userMessage }],
@@ -12092,7 +12161,6 @@ async function executeTool(page, screenshotter, toolName, toolInput, context) {
12092
12161
  const assertionType = toolInput.assertion_type;
12093
12162
  const selector = toolInput.selector;
12094
12163
  const expected = toolInput.expected;
12095
- const sessionId = context.sessionId ?? "default";
12096
12164
  switch (assertionType) {
12097
12165
  case "element_exists": {
12098
12166
  if (!selector)
@@ -12157,7 +12225,6 @@ async function executeTool(page, screenshotter, toolName, toolInput, context) {
12157
12225
  case "browser_intercept": {
12158
12226
  const action = toolInput.action;
12159
12227
  const pattern = toolInput.pattern;
12160
- const interceptAction = toolInput.intercept_action;
12161
12228
  const statusCode = toolInput.status_code;
12162
12229
  const body = toolInput.body;
12163
12230
  const sessionId = context.sessionId ?? "default";
@@ -12234,7 +12301,28 @@ ${JSON.stringify(har, null, 2)}` };
12234
12301
  }
12235
12302
  case "browser_a11y": {
12236
12303
  const level = toolInput.level ?? "AA";
12237
- const snapshot = await page.accessibility.snapshot();
12304
+ const snapshot = await page.evaluate(() => {
12305
+ function readRole(el) {
12306
+ return el.getAttribute("role") ?? el.tagName.toLowerCase();
12307
+ }
12308
+ function readName(el) {
12309
+ const labelledBy = el.getAttribute("aria-labelledby");
12310
+ if (labelledBy) {
12311
+ const labelledText = labelledBy.split(/\s+/).map((id) => document.getElementById(id)?.textContent?.trim()).filter(Boolean).join(" ");
12312
+ if (labelledText)
12313
+ return labelledText;
12314
+ }
12315
+ return el.getAttribute("aria-label") ?? el.getAttribute("alt") ?? el.textContent?.trim() ?? "";
12316
+ }
12317
+ function walk(el) {
12318
+ return {
12319
+ role: readRole(el),
12320
+ name: readName(el),
12321
+ children: Array.from(el.children).map((child) => walk(child))
12322
+ };
12323
+ }
12324
+ return document.body ? walk(document.body) : null;
12325
+ });
12238
12326
  if (!snapshot)
12239
12327
  return { result: "Error: could not capture accessibility tree" };
12240
12328
  const issues = [];
@@ -12276,6 +12364,38 @@ ${filtered.join(`
12276
12364
  return { result: `Error executing ${toolName}: ${message}` };
12277
12365
  }
12278
12366
  }
12367
+ function resolveStartUrl(baseUrl, targetPath) {
12368
+ try {
12369
+ return new URL(targetPath, baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`).toString();
12370
+ } catch {
12371
+ return `${baseUrl.replace(/\/+$/, "")}/${targetPath.replace(/^\/+/, "")}`;
12372
+ }
12373
+ }
12374
+ function buildScenarioUserMessage(scenario, baseUrl) {
12375
+ const userParts = [
12376
+ `**Scenario:** ${scenario.name}`,
12377
+ `**Description:** ${scenario.description}`
12378
+ ];
12379
+ if (baseUrl) {
12380
+ const normalizedBaseUrl = baseUrl.replace(/\/+$/, "");
12381
+ userParts.push(`**Base URL:** ${normalizedBaseUrl}`);
12382
+ if (scenario.targetPath) {
12383
+ userParts.push(`**Start URL:** ${resolveStartUrl(normalizedBaseUrl, scenario.targetPath)}`);
12384
+ }
12385
+ userParts.push("**Navigation Boundary:** Treat the Base URL as the application under test. Resolve relative paths and in-app navigation against this origin. Do not navigate to another host unless a step explicitly includes an absolute external URL.");
12386
+ }
12387
+ if (scenario.targetPath) {
12388
+ userParts.push(`**Target Path:** ${scenario.targetPath}`);
12389
+ }
12390
+ if (scenario.steps.length > 0) {
12391
+ userParts.push("**Steps:**");
12392
+ for (let i = 0;i < scenario.steps.length; i++) {
12393
+ userParts.push(`${i + 1}. ${scenario.steps[i]}`);
12394
+ }
12395
+ }
12396
+ return userParts.join(`
12397
+ `);
12398
+ }
12279
12399
  async function runAgentLoop(options) {
12280
12400
  const {
12281
12401
  client,
@@ -12285,6 +12405,7 @@ async function runAgentLoop(options) {
12285
12405
  model,
12286
12406
  runId,
12287
12407
  sessionId,
12408
+ baseUrl,
12288
12409
  maxTurns = 30,
12289
12410
  onStep,
12290
12411
  persona,
@@ -12332,21 +12453,7 @@ Instructions: ${persona.instructions}` : "",
12332
12453
  "- Verify both positive and negative states"
12333
12454
  ].join(`
12334
12455
  `) + personaSection;
12335
- const userParts = [
12336
- `**Scenario:** ${scenario.name}`,
12337
- `**Description:** ${scenario.description}`
12338
- ];
12339
- if (scenario.targetPath) {
12340
- userParts.push(`**Target Path:** ${scenario.targetPath}`);
12341
- }
12342
- if (scenario.steps.length > 0) {
12343
- userParts.push("**Steps:**");
12344
- for (let i = 0;i < scenario.steps.length; i++) {
12345
- userParts.push(`${i + 1}. ${scenario.steps[i]}`);
12346
- }
12347
- }
12348
- const userMessage = userParts.join(`
12349
- `);
12456
+ const userMessage = buildScenarioUserMessage(scenario, baseUrl);
12350
12457
  const screenshots = [];
12351
12458
  let tokensUsed = 0;
12352
12459
  let stepNumber = 0;
@@ -12409,7 +12516,7 @@ Instructions: ${persona.instructions}` : "",
12409
12516
  if (onStep) {
12410
12517
  onStep({ type: "tool_call", toolName: toolBlock.name, toolInput, stepNumber });
12411
12518
  }
12412
- const execResult = await executeTool(page, screenshotter, toolBlock.name, toolInput, { runId, scenarioSlug, stepNumber, sessionId, a11y });
12519
+ const execResult = await executeTool(page, screenshotter, toolBlock.name, toolInput, { runId, scenarioSlug, stepNumber, sessionId: sessionId ?? runId, a11y });
12413
12520
  if (onStep) {
12414
12521
  onStep({ type: "tool_result", toolName: toolBlock.name, toolResult: execResult.result, stepNumber });
12415
12522
  }
@@ -12460,10 +12567,17 @@ function detectProvider(model) {
12460
12567
  return "openai";
12461
12568
  if (model.startsWith("gemini-"))
12462
12569
  return "google";
12570
+ if (model.startsWith("glm-") || model.startsWith("zai/") || model.startsWith("zai-"))
12571
+ return "zai";
12463
12572
  if (model.startsWith("llama-") || model.startsWith("qwen-") || model.includes("cerebras"))
12464
12573
  return "cerebras";
12465
12574
  return "anthropic";
12466
12575
  }
12576
+ function resolveProviderApiKeyForModel(model, explicitApiKey, configuredAnthropicApiKey) {
12577
+ if (explicitApiKey)
12578
+ return explicitApiKey;
12579
+ return detectProvider(model) === "anthropic" ? configuredAnthropicApiKey : undefined;
12580
+ }
12467
12581
  function createClient(apiKey) {
12468
12582
  const key = apiKey ?? process.env["ANTHROPIC_API_KEY"];
12469
12583
  if (!key) {
@@ -12541,26 +12655,34 @@ async function callOpenAICompatible(options) {
12541
12655
  const usage = { input_tokens: data.usage?.prompt_tokens ?? 0, output_tokens: data.usage?.completion_tokens ?? 0 };
12542
12656
  return { content, stop_reason: stopReason, usage };
12543
12657
  }
12544
- function createClientForModel(model, apiKey) {
12545
- const provider = detectProvider(model);
12658
+ function createOpenAICompatibleConfig(provider, apiKey) {
12546
12659
  if (provider === "openai") {
12547
- const key = apiKey ?? process.env["OPENAI_API_KEY"];
12548
- if (!key)
12660
+ const key2 = apiKey ?? process.env["OPENAI_API_KEY"];
12661
+ if (!key2)
12549
12662
  throw new AIClientError("No OpenAI API key. Set OPENAI_API_KEY or pass it explicitly.");
12550
- return { provider: "openai", baseUrl: "https://api.openai.com/v1", apiKey: key };
12663
+ return { provider: "openai", baseUrl: "https://api.openai.com/v1", apiKey: key2 };
12551
12664
  }
12552
12665
  if (provider === "google") {
12553
- const key = apiKey ?? process.env["GOOGLE_API_KEY"];
12554
- if (!key)
12666
+ const key2 = apiKey ?? process.env["GOOGLE_API_KEY"];
12667
+ if (!key2)
12555
12668
  throw new AIClientError("No Google API key. Set GOOGLE_API_KEY or pass it explicitly.");
12556
- return { provider: "google", baseUrl: "https://generativelanguage.googleapis.com/v1beta/openai", apiKey: key };
12669
+ return { provider: "google", baseUrl: "https://generativelanguage.googleapis.com/v1beta/openai", apiKey: key2 };
12557
12670
  }
12558
12671
  if (provider === "cerebras") {
12559
- const key = apiKey ?? process.env["CEREBRAS_API_KEY"];
12560
- if (!key)
12672
+ const key2 = apiKey ?? process.env["CEREBRAS_API_KEY"];
12673
+ if (!key2)
12561
12674
  throw new AIClientError("No Cerebras API key. Set CEREBRAS_API_KEY or pass it explicitly.");
12562
- return { provider: "cerebras", baseUrl: "https://api.cerebras.ai/v1", apiKey: key };
12675
+ return { provider: "cerebras", baseUrl: "https://api.cerebras.ai/v1", apiKey: key2 };
12563
12676
  }
12677
+ const key = apiKey ?? process.env["ZAI_API_KEY"];
12678
+ if (!key)
12679
+ throw new AIClientError("No Z.AI API key. Set ZAI_API_KEY or pass it explicitly.");
12680
+ return { provider: "zai", baseUrl: "https://api.z.ai/api/paas/v4", apiKey: key };
12681
+ }
12682
+ function createClientForModel(model, apiKey) {
12683
+ const provider = detectProvider(model);
12684
+ if (provider !== "anthropic")
12685
+ return createOpenAICompatibleConfig(provider, apiKey);
12564
12686
  return createClient(apiKey);
12565
12687
  }
12566
12688
  var activeHARs, activeCoverage, BROWSER_TOOLS;
@@ -13608,6 +13730,127 @@ function updateLastRun(id, runId, nextRunAt) {
13608
13730
  UPDATE schedules SET last_run_id = ?, last_run_at = ?, next_run_at = ?, updated_at = ? WHERE id = ?
13609
13731
  `).run(runId, timestamp, nextRunAt, timestamp, id);
13610
13732
  }
13733
+ // src/db/workflows.ts
13734
+ init_types();
13735
+ init_database();
13736
+ var DEFAULT_EXECUTION = { target: "local" };
13737
+ function normalizeGoal(input) {
13738
+ if (!input)
13739
+ return null;
13740
+ const prompt = input.prompt?.trim();
13741
+ if (!prompt)
13742
+ return null;
13743
+ return {
13744
+ prompt,
13745
+ successCriteria: input.successCriteria ?? [],
13746
+ maxIterations: input.maxIterations ?? 10
13747
+ };
13748
+ }
13749
+ function normalizeFilter(input) {
13750
+ return {
13751
+ scenarioIds: input?.scenarioIds?.filter(Boolean),
13752
+ tags: input?.tags?.filter(Boolean),
13753
+ priority: input?.priority
13754
+ };
13755
+ }
13756
+ function normalizeExecution(input) {
13757
+ return input ? workflowExecutionFromValue(input) : DEFAULT_EXECUTION;
13758
+ }
13759
+ function createTestingWorkflow(input) {
13760
+ const db2 = getDatabase();
13761
+ const id = uuid();
13762
+ const timestamp = now();
13763
+ db2.query(`
13764
+ INSERT INTO testing_workflows
13765
+ (id, project_id, name, description, scenario_filter, persona_ids, goal, execution, settings, enabled, created_at, updated_at)
13766
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
13767
+ `).run(id, input.projectId ?? null, input.name, input.description ?? null, JSON.stringify(normalizeFilter(input.scenarioFilter)), JSON.stringify(input.personaIds ?? []), JSON.stringify(normalizeGoal(input.goal)), JSON.stringify(normalizeExecution(input.execution)), JSON.stringify(input.settings ?? {}), input.enabled === false ? 0 : 1, timestamp, timestamp);
13768
+ return getTestingWorkflow(id);
13769
+ }
13770
+ function getTestingWorkflow(id) {
13771
+ const db2 = getDatabase();
13772
+ let row = db2.query("SELECT * FROM testing_workflows WHERE id = ?").get(id);
13773
+ if (row)
13774
+ return workflowFromRow(row);
13775
+ const fullId = resolvePartialId("testing_workflows", id);
13776
+ if (fullId) {
13777
+ row = db2.query("SELECT * FROM testing_workflows WHERE id = ?").get(fullId);
13778
+ if (row)
13779
+ return workflowFromRow(row);
13780
+ }
13781
+ row = db2.query("SELECT * FROM testing_workflows WHERE name = ?").get(id);
13782
+ return row ? workflowFromRow(row) : null;
13783
+ }
13784
+ function listTestingWorkflows(filter) {
13785
+ const db2 = getDatabase();
13786
+ const conditions = [];
13787
+ const params = [];
13788
+ if (filter?.projectId) {
13789
+ conditions.push("project_id = ?");
13790
+ params.push(filter.projectId);
13791
+ }
13792
+ if (filter?.enabled !== undefined) {
13793
+ conditions.push("enabled = ?");
13794
+ params.push(filter.enabled ? 1 : 0);
13795
+ }
13796
+ const where = conditions.length > 0 ? ` WHERE ${conditions.join(" AND ")}` : "";
13797
+ const rows = db2.query(`SELECT * FROM testing_workflows${where} ORDER BY created_at DESC`).all(...params);
13798
+ return rows.map(workflowFromRow);
13799
+ }
13800
+ function updateTestingWorkflow(id, input) {
13801
+ const existing = getTestingWorkflow(id);
13802
+ if (!existing)
13803
+ throw new Error(`Testing workflow not found: ${id}`);
13804
+ const fields = [];
13805
+ const values = [];
13806
+ if (input.name !== undefined) {
13807
+ fields.push("name = ?");
13808
+ values.push(input.name);
13809
+ }
13810
+ if (input.description !== undefined) {
13811
+ fields.push("description = ?");
13812
+ values.push(input.description);
13813
+ }
13814
+ if (input.scenarioFilter !== undefined) {
13815
+ fields.push("scenario_filter = ?");
13816
+ values.push(JSON.stringify(normalizeFilter(input.scenarioFilter)));
13817
+ }
13818
+ if (input.personaIds !== undefined) {
13819
+ fields.push("persona_ids = ?");
13820
+ values.push(JSON.stringify(input.personaIds));
13821
+ }
13822
+ if (input.goal !== undefined) {
13823
+ fields.push("goal = ?");
13824
+ values.push(JSON.stringify(normalizeGoal(input.goal)));
13825
+ }
13826
+ if (input.execution !== undefined) {
13827
+ fields.push("execution = ?");
13828
+ values.push(JSON.stringify(normalizeExecution(input.execution)));
13829
+ }
13830
+ if (input.settings !== undefined) {
13831
+ fields.push("settings = ?");
13832
+ values.push(JSON.stringify(input.settings));
13833
+ }
13834
+ if (input.enabled !== undefined) {
13835
+ fields.push("enabled = ?");
13836
+ values.push(input.enabled ? 1 : 0);
13837
+ }
13838
+ if (fields.length === 0)
13839
+ return existing;
13840
+ fields.push("updated_at = ?");
13841
+ values.push(now(), existing.id);
13842
+ db2().query(`UPDATE testing_workflows SET ${fields.join(", ")} WHERE id = ?`).run(...values);
13843
+ return getTestingWorkflow(existing.id);
13844
+ }
13845
+ function deleteTestingWorkflow(id) {
13846
+ const existing = getTestingWorkflow(id);
13847
+ if (!existing)
13848
+ return false;
13849
+ return getDatabase().query("DELETE FROM testing_workflows WHERE id = ?").run(existing.id).changes > 0;
13850
+ }
13851
+ function db2() {
13852
+ return getDatabase();
13853
+ }
13611
13854
 
13612
13855
  // src/index.ts
13613
13856
  init_flows();
@@ -13878,11 +14121,11 @@ function resolveJudgeModel(config) {
13878
14121
  apiKey = process.env["GOOGLE_API_KEY"];
13879
14122
  else if (provider === "cerebras")
13880
14123
  apiKey = process.env["CEREBRAS_API_KEY"];
14124
+ else if (provider === "zai")
14125
+ apiKey = process.env["ZAI_API_KEY"];
13881
14126
  }
13882
14127
  if (!apiKey) {
13883
- apiKey = process.env["ANTHROPIC_API_KEY"] ?? process.env["CEREBRAS_API_KEY"] ?? process.env["OPENAI_API_KEY"] ?? process.env["GOOGLE_API_KEY"] ?? globalConfig.anthropicApiKey;
13884
- if (!apiKey)
13885
- throw new AIClientError("No API key found for judge. Set ANTHROPIC_API_KEY, CEREBRAS_API_KEY, OPENAI_API_KEY, or GOOGLE_API_KEY.");
14128
+ throw new AIClientError(`No API key found for ${provider} judge provider.`);
13886
14129
  }
13887
14130
  return { model, provider, apiKey };
13888
14131
  }
@@ -13897,11 +14140,11 @@ reason: 1-2 sentences max`;
13897
14140
  async function callJudge(prompt, config) {
13898
14141
  const { model, provider, apiKey } = resolveJudgeModel(config);
13899
14142
  const threshold = 0.7;
13900
- if (provider === "openai" || provider === "google" || provider === "cerebras") {
13901
- const baseUrl = provider === "openai" ? "https://api.openai.com/v1" : provider === "cerebras" ? "https://api.cerebras.ai/v1" : "https://generativelanguage.googleapis.com/v1beta/openai";
14143
+ if (provider !== "anthropic") {
14144
+ const compat = createOpenAICompatibleConfig(provider, apiKey);
13902
14145
  const resp2 = await callOpenAICompatible({
13903
- baseUrl,
13904
- apiKey,
14146
+ baseUrl: compat.baseUrl,
14147
+ apiKey: compat.apiKey,
13905
14148
  model,
13906
14149
  system: LLM_SYSTEM,
13907
14150
  messages: [{ role: "user", content: prompt }],
@@ -14905,20 +15148,20 @@ function loadBudgetConfig() {
14905
15148
  };
14906
15149
  }
14907
15150
  function getCostSummary(options) {
14908
- const db2 = getDatabase();
15151
+ const db3 = getDatabase();
14909
15152
  const period = options?.period ?? "month";
14910
15153
  const projectId = options?.projectId;
14911
15154
  const dateFilter = getDateFilter(period);
14912
15155
  const projectFilter = projectId ? "AND ru.project_id = ?" : "";
14913
15156
  const projectParams = projectId ? [projectId] : [];
14914
- const totalsRow = db2.query(`SELECT
15157
+ const totalsRow = db3.query(`SELECT
14915
15158
  COALESCE(SUM(r.cost_cents), 0) as total_cost,
14916
15159
  COALESCE(SUM(r.tokens_used), 0) as total_tokens,
14917
15160
  COUNT(DISTINCT r.run_id) as run_count
14918
15161
  FROM results r
14919
15162
  JOIN runs ru ON r.run_id = ru.id
14920
15163
  WHERE 1=1 ${dateFilter} ${projectFilter}`).get(...projectParams);
14921
- const modelRows = db2.query(`SELECT
15164
+ const modelRows = db3.query(`SELECT
14922
15165
  r.model,
14923
15166
  COALESCE(SUM(r.cost_cents), 0) as cost_cents,
14924
15167
  COALESCE(SUM(r.tokens_used), 0) as tokens,
@@ -14936,7 +15179,7 @@ function getCostSummary(options) {
14936
15179
  runs: row.runs
14937
15180
  };
14938
15181
  }
14939
- const scenarioRows = db2.query(`SELECT
15182
+ const scenarioRows = db3.query(`SELECT
14940
15183
  r.scenario_id,
14941
15184
  COALESCE(s.name, r.scenario_id) as name,
14942
15185
  COALESCE(SUM(r.cost_cents), 0) as cost_cents,
@@ -15088,22 +15331,22 @@ function formatCostsJSON(summary) {
15088
15331
  // src/db/step-results.ts
15089
15332
  init_database();
15090
15333
  function createStepResult(input) {
15091
- const db2 = getDatabase();
15334
+ const db3 = getDatabase();
15092
15335
  const id = uuid();
15093
15336
  const timestamp = now();
15094
- db2.query(`
15337
+ db3.query(`
15095
15338
  INSERT INTO step_results (id, result_id, step_number, action, status, tool_name, tool_input, thinking, created_at)
15096
15339
  VALUES (?, ?, ?, ?, 'running', ?, ?, ?, ?)
15097
15340
  `).run(id, input.resultId, input.stepNumber, input.action, input.toolName ?? null, input.toolInput ? JSON.stringify(input.toolInput) : null, input.thinking ?? null, timestamp);
15098
15341
  return getStepResult(id);
15099
15342
  }
15100
15343
  function getStepResult(id) {
15101
- const db2 = getDatabase();
15102
- const row = db2.query("SELECT * FROM step_results WHERE id = ?").get(id);
15344
+ const db3 = getDatabase();
15345
+ const row = db3.query("SELECT * FROM step_results WHERE id = ?").get(id);
15103
15346
  return row ? stepResultFromRow(row) : null;
15104
15347
  }
15105
15348
  function updateStepResult(id, updates) {
15106
- const db2 = getDatabase();
15349
+ const db3 = getDatabase();
15107
15350
  const existing = getStepResult(id);
15108
15351
  if (!existing)
15109
15352
  return null;
@@ -15132,7 +15375,7 @@ function updateStepResult(id, updates) {
15132
15375
  if (sets.length === 0)
15133
15376
  return existing;
15134
15377
  params.push(id);
15135
- db2.query(`UPDATE step_results SET ${sets.join(", ")} WHERE id = ?`).run(...params);
15378
+ db3.query(`UPDATE step_results SET ${sets.join(", ")} WHERE id = ?`).run(...params);
15136
15379
  return getStepResult(id);
15137
15380
  }
15138
15381
  function stepResultFromRow(row) {
@@ -15157,18 +15400,18 @@ function stepResultFromRow(row) {
15157
15400
  init_types();
15158
15401
  init_database();
15159
15402
  function getPersona(id) {
15160
- const db2 = getDatabase();
15161
- let row = db2.query("SELECT * FROM personas WHERE id = ?").get(id);
15403
+ const db3 = getDatabase();
15404
+ let row = db3.query("SELECT * FROM personas WHERE id = ?").get(id);
15162
15405
  if (row)
15163
15406
  return personaFromRow(row);
15164
- row = db2.query("SELECT * FROM personas WHERE short_id = ?").get(id);
15407
+ row = db3.query("SELECT * FROM personas WHERE short_id = ?").get(id);
15165
15408
  if (row)
15166
15409
  return personaFromRow(row);
15167
15410
  return null;
15168
15411
  }
15169
15412
  function savePersonaAuthCookies(id, cookies) {
15170
- const db2 = getDatabase();
15171
- db2.query("UPDATE personas SET auth_cookies = ?, updated_at = ? WHERE id = ?").run(JSON.stringify(cookies), now(), id);
15413
+ const db3 = getDatabase();
15414
+ db3.query("UPDATE personas SET auth_cookies = ?, updated_at = ? WHERE id = ?").run(JSON.stringify(cookies), now(), id);
15172
15415
  }
15173
15416
 
15174
15417
  // src/lib/runner.ts
@@ -15192,9 +15435,9 @@ function lookupFromVault(key) {
15192
15435
  if (!existsSync9(vaultPath))
15193
15436
  return null;
15194
15437
  try {
15195
- const db2 = new Database2(vaultPath, { readonly: true });
15196
- const row = db2.query("SELECT value FROM secrets WHERE key = ?").get(key);
15197
- db2.close();
15438
+ const db3 = new Database2(vaultPath, { readonly: true });
15439
+ const row = db3.query("SELECT value FROM secrets WHERE key = ?").get(key);
15440
+ db3.close();
15198
15441
  return row?.value ?? null;
15199
15442
  } catch {
15200
15443
  return null;
@@ -15458,21 +15701,21 @@ function fromRow(row) {
15458
15701
  };
15459
15702
  }
15460
15703
  function createWebhook(input) {
15461
- const db2 = getDatabase();
15704
+ const db3 = getDatabase();
15462
15705
  const id = uuid();
15463
15706
  const events = input.events ?? ["failed"];
15464
15707
  const secret = input.secret ?? crypto.randomUUID().replace(/-/g, "");
15465
- db2.query(`
15708
+ db3.query(`
15466
15709
  INSERT INTO webhooks (id, url, events, project_id, secret, active, created_at)
15467
15710
  VALUES (?, ?, ?, ?, ?, 1, ?)
15468
15711
  `).run(id, input.url, JSON.stringify(events), input.projectId ?? null, secret, now());
15469
15712
  return getWebhook(id);
15470
15713
  }
15471
15714
  function getWebhook(id) {
15472
- const db2 = getDatabase();
15473
- const row = db2.query("SELECT * FROM webhooks WHERE id = ?").get(id);
15715
+ const db3 = getDatabase();
15716
+ const row = db3.query("SELECT * FROM webhooks WHERE id = ?").get(id);
15474
15717
  if (!row) {
15475
- const rows = db2.query("SELECT * FROM webhooks WHERE id LIKE ? || '%'").all(id);
15718
+ const rows = db3.query("SELECT * FROM webhooks WHERE id LIKE ? || '%'").all(id);
15476
15719
  if (rows.length === 1)
15477
15720
  return fromRow(rows[0]);
15478
15721
  return null;
@@ -15480,7 +15723,7 @@ function getWebhook(id) {
15480
15723
  return fromRow(row);
15481
15724
  }
15482
15725
  function listWebhooks(projectId) {
15483
- const db2 = getDatabase();
15726
+ const db3 = getDatabase();
15484
15727
  let query = "SELECT * FROM webhooks WHERE active = 1";
15485
15728
  const params = [];
15486
15729
  if (projectId) {
@@ -15488,15 +15731,15 @@ function listWebhooks(projectId) {
15488
15731
  params.push(projectId);
15489
15732
  }
15490
15733
  query += " ORDER BY created_at DESC";
15491
- const rows = db2.query(query).all(...params);
15734
+ const rows = db3.query(query).all(...params);
15492
15735
  return rows.map(fromRow);
15493
15736
  }
15494
15737
  function deleteWebhook(id) {
15495
- const db2 = getDatabase();
15738
+ const db3 = getDatabase();
15496
15739
  const webhook = getWebhook(id);
15497
15740
  if (!webhook)
15498
15741
  return false;
15499
- db2.query("DELETE FROM webhooks WHERE id = ?").run(webhook.id);
15742
+ db3.query("DELETE FROM webhooks WHERE id = ?").run(webhook.id);
15500
15743
  return true;
15501
15744
  }
15502
15745
  function signPayload(body, secret) {
@@ -15664,12 +15907,12 @@ function connectToTodos(options = {}) {
15664
15907
  if (!existsSync10(dbPath)) {
15665
15908
  throw new TodosConnectionError(`Todos database not found at ${dbPath}. Install @hasna/todos or set TODOS_DB_PATH.`);
15666
15909
  }
15667
- const db2 = new Database3(dbPath, { readonly: options.readonly ?? true });
15668
- db2.exec("PRAGMA foreign_keys = ON");
15669
- return db2;
15910
+ const db3 = new Database3(dbPath, { readonly: options.readonly ?? true });
15911
+ db3.exec("PRAGMA foreign_keys = ON");
15912
+ return db3;
15670
15913
  }
15671
15914
  function pullTasks(options = {}) {
15672
- const db2 = connectToTodos({ readonly: true });
15915
+ const db3 = connectToTodos({ readonly: true });
15673
15916
  try {
15674
15917
  let query = "SELECT id, short_id, title, description, status, priority, tags, project_id FROM tasks WHERE 1=1";
15675
15918
  const params = [];
@@ -15684,14 +15927,14 @@ function pullTasks(options = {}) {
15684
15927
  params.push(options.priority);
15685
15928
  }
15686
15929
  if (options.projectName) {
15687
- const project = db2.query("SELECT id FROM projects WHERE name = ?").get(options.projectName);
15930
+ const project = db3.query("SELECT id FROM projects WHERE name = ?").get(options.projectName);
15688
15931
  if (project) {
15689
15932
  query += " AND project_id = ?";
15690
15933
  params.push(project.id);
15691
15934
  }
15692
15935
  }
15693
15936
  query += " ORDER BY CASE priority WHEN 'critical' THEN 0 WHEN 'high' THEN 1 WHEN 'medium' THEN 2 WHEN 'low' THEN 3 END";
15694
- const tasks = db2.query(query).all(...params);
15937
+ const tasks = db3.query(query).all(...params);
15695
15938
  if (options.tags && options.tags.length > 0) {
15696
15939
  return tasks.filter((task) => {
15697
15940
  const taskTags = JSON.parse(task.tags || "[]");
@@ -15700,7 +15943,7 @@ function pullTasks(options = {}) {
15700
15943
  }
15701
15944
  return tasks;
15702
15945
  } finally {
15703
- db2.close();
15946
+ db3.close();
15704
15947
  }
15705
15948
  }
15706
15949
  function taskToScenarioInput(task, projectId) {
@@ -15752,15 +15995,15 @@ function markTodoDone(taskId) {
15752
15995
  const dbPath = resolveTodosDbPath();
15753
15996
  if (!existsSync10(dbPath))
15754
15997
  return false;
15755
- const db2 = new Database3(dbPath);
15998
+ const db3 = new Database3(dbPath);
15756
15999
  try {
15757
- const task = db2.query("SELECT id, version FROM tasks WHERE id LIKE ? || '%'").get(taskId);
16000
+ const task = db3.query("SELECT id, version FROM tasks WHERE id LIKE ? || '%'").get(taskId);
15758
16001
  if (!task)
15759
16002
  return false;
15760
- db2.query("UPDATE tasks SET status = 'completed', completed_at = datetime('now'), version = version + 1, updated_at = datetime('now') WHERE id = ? AND version = ?").run(task.id, task.version);
16003
+ db3.query("UPDATE tasks SET status = 'completed', completed_at = datetime('now'), version = version + 1, updated_at = datetime('now') WHERE id = ? AND version = ?").run(task.id, task.version);
15761
16004
  return true;
15762
16005
  } finally {
15763
- db2.close();
16006
+ db3.close();
15764
16007
  }
15765
16008
  }
15766
16009
 
@@ -15771,9 +16014,9 @@ async function createFailureTasks(run, failedResults, scenarios) {
15771
16014
  const projectId = process.env["TESTERS_TODOS_PROJECT_ID"];
15772
16015
  if (!projectId)
15773
16016
  return { created: 0, skipped: 0 };
15774
- let db2 = null;
16017
+ let db3 = null;
15775
16018
  try {
15776
- db2 = connectToTodos({ readonly: false });
16019
+ db3 = connectToTodos({ readonly: false });
15777
16020
  } catch {
15778
16021
  return { created: 0, skipped: 0 };
15779
16022
  }
@@ -15784,7 +16027,7 @@ async function createFailureTasks(run, failedResults, scenarios) {
15784
16027
  for (const result of failedResults) {
15785
16028
  const scenario = scenarioMap.get(result.scenarioId);
15786
16029
  const title = `BUG: [testers] ${scenario?.name ?? result.scenarioId} failed`;
15787
- const existing = db2.query("SELECT id FROM tasks WHERE title = ? AND status NOT IN ('completed', 'cancelled') LIMIT 1").get(title);
16030
+ const existing = db3.query("SELECT id FROM tasks WHERE title = ? AND status NOT IN ('completed', 'cancelled') LIMIT 1").get(title);
15788
16031
  if (existing) {
15789
16032
  skipped++;
15790
16033
  continue;
@@ -15805,7 +16048,7 @@ async function createFailureTasks(run, failedResults, scenarios) {
15805
16048
  ].filter(Boolean).join(`
15806
16049
  `);
15807
16050
  try {
15808
- db2.query(`
16051
+ db3.query(`
15809
16052
  INSERT INTO tasks (id, short_id, title, description, status, priority, tags, project_id, version, created_at, updated_at)
15810
16053
  VALUES (?, ?, ?, ?, 'pending', 'high', ?, ?, 1, ?, ?)
15811
16054
  `).run(id, `BUG-${id.slice(0, 6)}`, title, description, JSON.stringify(["bug", "testers", "auto-created"]), projectId, now2, now2);
@@ -15815,7 +16058,7 @@ async function createFailureTasks(run, failedResults, scenarios) {
15815
16058
  }
15816
16059
  }
15817
16060
  } finally {
15818
- db2.close();
16061
+ db3.close();
15819
16062
  }
15820
16063
  return { created, skipped };
15821
16064
  }
@@ -15894,6 +16137,291 @@ async function notifyRunToConversations(run, results, options) {
15894
16137
  } catch {}
15895
16138
  }
15896
16139
 
16140
+ // src/lib/a11y-audit.ts
16141
+ async function runA11yAudit(page, options = {}) {
16142
+ const { level = "AA", rules, exclude = [] } = options;
16143
+ await page.addScriptTag({ url: "https://cdnjs.cloudflare.com/ajax/libs/axe-core/4.9.1/axe.min.js" });
16144
+ const config = {
16145
+ runOnly: {
16146
+ type: level === "AAA" ? "standard" : "tag",
16147
+ values: level === "AAA" ? undefined : [level, "best-practice"]
16148
+ }
16149
+ };
16150
+ if (rules && rules.length > 0) {
16151
+ config.rules = Object.fromEntries(rules.map((r) => [r, { enabled: true }]));
16152
+ }
16153
+ if (exclude.length > 0) {
16154
+ config.exclude = exclude;
16155
+ }
16156
+ const result = await page.evaluate(async (auditConfig) => {
16157
+ const axeResult = await window.axe.run(auditConfig);
16158
+ return axeResult;
16159
+ }, config);
16160
+ const violations = (result.violations ?? []).map((v) => ({
16161
+ id: v.id,
16162
+ impact: v.impact,
16163
+ description: v.description,
16164
+ help: v.help,
16165
+ helpUrl: v.helpUrl,
16166
+ nodes: (v.nodes ?? []).map((n) => ({
16167
+ html: n.html,
16168
+ target: n.target,
16169
+ failureSummary: n.failureSummary
16170
+ }))
16171
+ }));
16172
+ const passes = (result.passes ?? []).map((p) => ({
16173
+ id: p.id,
16174
+ description: p.description
16175
+ }));
16176
+ const incomplete = (result.incomplete ?? []).map((i) => ({
16177
+ id: i.id,
16178
+ description: i.description,
16179
+ impact: i.impact
16180
+ }));
16181
+ const criticalCount = violations.filter((v) => v.impact === "critical").length;
16182
+ const seriousCount = violations.filter((v) => v.impact === "serious").length;
16183
+ const moderateCount = violations.filter((v) => v.impact === "moderate").length;
16184
+ const minorCount = violations.filter((v) => v.impact === "minor").length;
16185
+ return {
16186
+ violations,
16187
+ passes,
16188
+ incomplete,
16189
+ url: page.url(),
16190
+ timestamp: new Date().toISOString(),
16191
+ totalViolations: violations.length,
16192
+ criticalCount,
16193
+ seriousCount,
16194
+ moderateCount,
16195
+ minorCount
16196
+ };
16197
+ }
16198
+
16199
+ // src/lib/assertions.ts
16200
+ async function evaluateAssertions(page, assertions, context = {}) {
16201
+ const results = [];
16202
+ for (const assertion of assertions) {
16203
+ try {
16204
+ const result = await evaluateOne(page, assertion, context);
16205
+ results.push(result);
16206
+ } catch (err) {
16207
+ results.push({
16208
+ assertion,
16209
+ passed: false,
16210
+ actual: "",
16211
+ error: err instanceof Error ? err.message : String(err)
16212
+ });
16213
+ }
16214
+ }
16215
+ return results;
16216
+ }
16217
+ async function evaluateOne(page, assertion, context) {
16218
+ switch (assertion.type) {
16219
+ case "visible": {
16220
+ const visible = await page.locator(assertion.selector).isVisible();
16221
+ return {
16222
+ assertion,
16223
+ passed: visible,
16224
+ actual: String(visible)
16225
+ };
16226
+ }
16227
+ case "not_visible": {
16228
+ const visible = await page.locator(assertion.selector).isVisible();
16229
+ return {
16230
+ assertion,
16231
+ passed: !visible,
16232
+ actual: String(visible)
16233
+ };
16234
+ }
16235
+ case "text_contains": {
16236
+ const text = await page.locator(assertion.selector).textContent() ?? "";
16237
+ const expected = String(assertion.expected ?? "");
16238
+ return {
16239
+ assertion,
16240
+ passed: text.includes(expected),
16241
+ actual: text
16242
+ };
16243
+ }
16244
+ case "text_equals": {
16245
+ const text = await page.locator(assertion.selector).textContent() ?? "";
16246
+ const expected = String(assertion.expected ?? "");
16247
+ return {
16248
+ assertion,
16249
+ passed: text.trim() === expected.trim(),
16250
+ actual: text
16251
+ };
16252
+ }
16253
+ case "element_count": {
16254
+ const count = await page.locator(assertion.selector).count();
16255
+ const expected = Number(assertion.expected ?? 0);
16256
+ return {
16257
+ assertion,
16258
+ passed: count === expected,
16259
+ actual: String(count)
16260
+ };
16261
+ }
16262
+ case "no_console_errors": {
16263
+ if (context.consoleErrors !== undefined) {
16264
+ const errors = context.consoleErrors.filter(Boolean);
16265
+ return {
16266
+ assertion,
16267
+ passed: errors.length === 0,
16268
+ actual: errors.length === 0 ? "No console errors captured" : errors.slice(0, 3).join(" | ")
16269
+ };
16270
+ }
16271
+ const errorElements = await page.locator('[role="alert"], .error, .error-message, [data-testid="error"]').count();
16272
+ return {
16273
+ assertion,
16274
+ passed: errorElements === 0,
16275
+ actual: `${errorElements} error element(s) found`
16276
+ };
16277
+ }
16278
+ case "no_a11y_violations": {
16279
+ try {
16280
+ const auditResult = await runA11yAudit(page);
16281
+ const hasIssues = auditResult.violations.length > 0;
16282
+ return {
16283
+ assertion,
16284
+ passed: !hasIssues,
16285
+ actual: hasIssues ? `${auditResult.totalViolations} violation(s): ${auditResult.violations.map((v) => v.id).join(", ")}` : "No accessibility violations found"
16286
+ };
16287
+ } catch (err) {
16288
+ return {
16289
+ assertion,
16290
+ passed: false,
16291
+ actual: "",
16292
+ error: err instanceof Error ? err.message : String(err)
16293
+ };
16294
+ }
16295
+ }
16296
+ case "url_contains": {
16297
+ const url = page.url();
16298
+ const expected = String(assertion.expected ?? "");
16299
+ return {
16300
+ assertion,
16301
+ passed: url.includes(expected),
16302
+ actual: url
16303
+ };
16304
+ }
16305
+ case "title_contains": {
16306
+ const title = await page.title();
16307
+ const expected = String(assertion.expected ?? "");
16308
+ return {
16309
+ assertion,
16310
+ passed: title.includes(expected),
16311
+ actual: title
16312
+ };
16313
+ }
16314
+ case "cookie_exists": {
16315
+ const cookieName = assertion.expected;
16316
+ const cookies = await page.context().cookies();
16317
+ const found = cookies.some((c) => c.name === cookieName);
16318
+ return {
16319
+ assertion,
16320
+ passed: found,
16321
+ actual: found ? `Cookie "${cookieName}" exists` : `Cookie "${cookieName}" not found`
16322
+ };
16323
+ }
16324
+ case "cookie_not_exists": {
16325
+ const cookieName = assertion.expected;
16326
+ const cookies = await page.context().cookies();
16327
+ const found = cookies.some((c) => c.name === cookieName);
16328
+ return {
16329
+ assertion,
16330
+ passed: !found,
16331
+ actual: found ? `Cookie "${cookieName}" found (unexpected)` : `Cookie "${cookieName}" does not exist`
16332
+ };
16333
+ }
16334
+ case "cookie_value": {
16335
+ const [cookieName, expectedValue] = assertion.expected.split("=", 2);
16336
+ const cookies = await page.context().cookies();
16337
+ const cookie = cookies.find((c) => c.name === cookieName);
16338
+ const actualValue = cookie?.value ?? "";
16339
+ return {
16340
+ assertion,
16341
+ passed: actualValue === expectedValue,
16342
+ actual: cookie ? `${cookieName}=${actualValue}` : `Cookie "${cookieName}" not found`
16343
+ };
16344
+ }
16345
+ case "local_storage_exists": {
16346
+ const key = assertion.expected;
16347
+ const value = await page.evaluate((k) => localStorage.getItem(k), key);
16348
+ return {
16349
+ assertion,
16350
+ passed: value !== null,
16351
+ actual: value !== null ? `Key "${key}" exists with value "${value}"` : `Key "${key}" not found in localStorage`
16352
+ };
16353
+ }
16354
+ case "local_storage_not_exists": {
16355
+ const key = assertion.expected;
16356
+ const value = await page.evaluate((k) => localStorage.getItem(k), key);
16357
+ return {
16358
+ assertion,
16359
+ passed: value === null,
16360
+ actual: value !== null ? `Key "${key}" exists (unexpected)` : `Key "${key}" does not exist in localStorage`
16361
+ };
16362
+ }
16363
+ case "local_storage_value": {
16364
+ const [lsKey, expectedValue] = assertion.expected.split("=", 2);
16365
+ const value = await page.evaluate((k) => localStorage.getItem(k), lsKey ?? "");
16366
+ return {
16367
+ assertion,
16368
+ passed: value === expectedValue,
16369
+ actual: value !== null ? `${lsKey}=${value}` : `Key "${lsKey}" not found in localStorage`
16370
+ };
16371
+ }
16372
+ case "session_storage_value": {
16373
+ const [ssKey, expectedValue] = assertion.expected.split("=", 2);
16374
+ const value = await page.evaluate((k) => sessionStorage.getItem(k), ssKey ?? "");
16375
+ return {
16376
+ assertion,
16377
+ passed: value === expectedValue,
16378
+ actual: value !== null ? `${ssKey}=${value}` : `Key "${ssKey}" not found in sessionStorage`
16379
+ };
16380
+ }
16381
+ case "session_storage_not_exists": {
16382
+ const key = assertion.expected;
16383
+ const value = await page.evaluate((k) => sessionStorage.getItem(k), key);
16384
+ return {
16385
+ assertion,
16386
+ passed: value === null,
16387
+ actual: value !== null ? `Key "${key}" exists (unexpected)` : `Key "${key}" does not exist in sessionStorage`
16388
+ };
16389
+ }
16390
+ default: {
16391
+ return {
16392
+ assertion,
16393
+ passed: false,
16394
+ actual: "",
16395
+ error: `Unknown assertion type: ${assertion.type}`
16396
+ };
16397
+ }
16398
+ }
16399
+ }
16400
+ function allAssertionsPassed(results) {
16401
+ return results.every((r) => r.passed);
16402
+ }
16403
+ function formatAssertionResults(results) {
16404
+ if (results.length === 0)
16405
+ return "No assertions.";
16406
+ const lines = [];
16407
+ for (const r of results) {
16408
+ const icon = r.passed ? "PASS" : "FAIL";
16409
+ const desc = r.assertion.description || `${r.assertion.type}${r.assertion.selector ? ` ${r.assertion.selector}` : ""}`;
16410
+ let line = ` [${icon}] ${desc}`;
16411
+ if (!r.passed) {
16412
+ line += ` (actual: ${r.actual})`;
16413
+ if (r.error)
16414
+ line += ` \u2014 ${r.error}`;
16415
+ }
16416
+ lines.push(line);
16417
+ }
16418
+ const passed = results.filter((r) => r.passed).length;
16419
+ lines.push(`
16420
+ ${passed}/${results.length} assertions passed.`);
16421
+ return lines.join(`
16422
+ `);
16423
+ }
16424
+
15897
16425
  // src/lib/runner.ts
15898
16426
  var eventHandler = null;
15899
16427
  function onRunEvent(handler) {
@@ -15903,6 +16431,57 @@ function emit(event) {
15903
16431
  if (eventHandler)
15904
16432
  eventHandler(event);
15905
16433
  }
16434
+ function resolveAgentApiKeyForModel(model, explicitApiKey, configuredAnthropicApiKey) {
16435
+ return resolveProviderApiKeyForModel(model, explicitApiKey, configuredAnthropicApiKey);
16436
+ }
16437
+ function assertionDescription(result) {
16438
+ return result.assertion.description || `${result.assertion.type}${result.assertion.selector ? ` ${result.assertion.selector}` : ""}`;
16439
+ }
16440
+ function summarizeAssertionResult(result) {
16441
+ const description = assertionDescription(result);
16442
+ if (result.passed)
16443
+ return description;
16444
+ const suffix = result.error ? `; ${result.error}` : "";
16445
+ return `${description} (actual: ${result.actual}${suffix})`;
16446
+ }
16447
+ async function applyStructuredAssertionsToResult(input) {
16448
+ const assertions = input.scenario.assertions ?? [];
16449
+ if (assertions.length === 0) {
16450
+ return {
16451
+ status: input.status,
16452
+ reasoning: input.reasoning,
16453
+ assertionsPassed: [],
16454
+ assertionsFailed: [],
16455
+ assertionResults: []
16456
+ };
16457
+ }
16458
+ const results = await evaluateAssertions(input.page, assertions, {
16459
+ consoleErrors: input.consoleErrors
16460
+ });
16461
+ const assertionsPassed = results.filter((r) => r.passed).map(summarizeAssertionResult);
16462
+ const assertionsFailed = results.filter((r) => !r.passed).map(summarizeAssertionResult);
16463
+ const assertionResults = results.map((result) => ({
16464
+ type: result.assertion.type,
16465
+ description: assertionDescription(result),
16466
+ passed: result.passed,
16467
+ actual: result.actual,
16468
+ ...result.error ? { error: result.error } : {}
16469
+ }));
16470
+ const assertionsOk = allAssertionsPassed(results);
16471
+ const status = assertionsOk || input.status !== "passed" ? input.status : "failed";
16472
+ const assertionHeading = assertionsOk ? "Structured assertions passed:" : "Structured assertions failed:";
16473
+ const reasoningParts = [input.reasoning, `${assertionHeading}
16474
+ ${formatAssertionResults(results)}`].map((part) => part.trim()).filter(Boolean);
16475
+ return {
16476
+ status,
16477
+ reasoning: reasoningParts.join(`
16478
+
16479
+ `),
16480
+ assertionsPassed,
16481
+ assertionsFailed,
16482
+ assertionResults
16483
+ };
16484
+ }
15906
16485
  function withTimeout(promise, ms, label) {
15907
16486
  return new Promise((resolve, reject) => {
15908
16487
  const warningAt = Math.floor(ms * 0.8);
@@ -15963,7 +16542,7 @@ async function runSingleScenario(scenario, runId, options) {
15963
16542
  });
15964
16543
  }
15965
16544
  }
15966
- const client = createClientForModel(model, effectiveOptions.apiKey ?? config.anthropicApiKey);
16545
+ const client = createClientForModel(model, resolveAgentApiKeyForModel(model, effectiveOptions.apiKey, config.anthropicApiKey));
15967
16546
  const screenshotter = new Screenshotter({
15968
16547
  baseDir: effectiveOptions.screenshotDir ?? config.screenshots.dir
15969
16548
  });
@@ -16073,6 +16652,7 @@ async function runSingleScenario(scenario, runId, options) {
16073
16652
  model,
16074
16653
  runId,
16075
16654
  sessionId: result.id,
16655
+ baseUrl: options.url,
16076
16656
  maxTurns: effectiveOptions.minimal ? 10 : 30,
16077
16657
  a11y: effectiveOptions.a11y,
16078
16658
  persona: persona ? {
@@ -16155,27 +16735,46 @@ async function runSingleScenario(scenario, runId, options) {
16155
16735
  closeSession(result.id);
16156
16736
  const lightpandaNote = options.engine === "lightpanda" ? " (Running with Lightpanda \u2014 no screenshots)" : options.engine === "bun" ? " (Running with Bun.WebView \u2014 native, ~11x faster)" : "";
16157
16737
  const networkMeta = networkErrors.length > 0 ? { networkErrors: networkErrors.slice(0, 20) } : {};
16158
- let updatedResult = updateResult(result.id, {
16738
+ const baseReasoning = agentResult.reasoning ? agentResult.reasoning + lightpandaNote : lightpandaNote || "";
16739
+ const assertionOutcome = await applyStructuredAssertionsToResult({
16740
+ page,
16741
+ scenario,
16742
+ consoleErrors,
16159
16743
  status: agentResult.status,
16160
- reasoning: agentResult.reasoning ? agentResult.reasoning + lightpandaNote : lightpandaNote || undefined,
16744
+ reasoning: baseReasoning
16745
+ });
16746
+ const structuredAssertionMeta = assertionOutcome.assertionResults.length > 0 ? {
16747
+ structuredAssertions: {
16748
+ passed: assertionOutcome.assertionsPassed,
16749
+ failed: assertionOutcome.assertionsFailed,
16750
+ results: assertionOutcome.assertionResults
16751
+ }
16752
+ } : {};
16753
+ let updatedResult = updateResult(result.id, {
16754
+ status: assertionOutcome.status,
16755
+ reasoning: assertionOutcome.reasoning || undefined,
16161
16756
  stepsCompleted: agentResult.stepsCompleted,
16162
16757
  durationMs: Date.now() - new Date(result.createdAt).getTime(),
16163
16758
  tokensUsed: agentResult.tokensUsed,
16164
16759
  costCents: estimateCost(model, agentResult.tokensUsed),
16165
- metadata: { consoleLogs, ...networkErrors.length > 0 ? networkMeta : {} }
16760
+ metadata: {
16761
+ consoleLogs,
16762
+ ...networkErrors.length > 0 ? networkMeta : {},
16763
+ ...structuredAssertionMeta
16764
+ }
16166
16765
  });
16167
- if (agentResult.status === "failed" || agentResult.status === "error") {
16168
- const failureAnalysis = analyzeFailure(null, agentResult.reasoning ?? null);
16766
+ if (assertionOutcome.status === "failed" || assertionOutcome.status === "error") {
16767
+ const failureAnalysis = analyzeFailure(null, assertionOutcome.reasoning ?? null);
16169
16768
  if (failureAnalysis) {
16170
16769
  updatedResult = updateResult(result.id, { failureAnalysis });
16171
16770
  }
16172
16771
  }
16173
- if (agentResult.status === "passed") {
16772
+ if (assertionOutcome.status === "passed") {
16174
16773
  try {
16175
16774
  updateScenarioPassedCache(scenario.id, options.url);
16176
16775
  } catch {}
16177
16776
  }
16178
- const eventType = agentResult.status === "passed" ? "scenario:pass" : "scenario:fail";
16777
+ const eventType = assertionOutcome.status === "passed" ? "scenario:pass" : "scenario:fail";
16179
16778
  emit({ type: eventType, scenarioId: scenario.id, scenarioName: scenario.name, resultId: result.id, runId });
16180
16779
  return updatedResult;
16181
16780
  } catch (error) {
@@ -16200,7 +16799,8 @@ async function runSingleScenario(scenario, runId, options) {
16200
16799
  } finally {
16201
16800
  if (harPath) {
16202
16801
  try {
16203
- updateResult(result.id, { metadata: { harPath } });
16802
+ const existing = getResult(result.id);
16803
+ updateResult(result.id, { metadata: { ...existing?.metadata ?? {}, harPath } });
16204
16804
  } catch {}
16205
16805
  }
16206
16806
  if (browser) {
@@ -16372,22 +16972,31 @@ async function runBatch(scenarios, options) {
16372
16972
  }
16373
16973
  return { run: finalRun, results };
16374
16974
  }
16375
- async function runByFilter(options) {
16376
- let scenarios;
16975
+ function findScenarioInList(scenarios, id) {
16976
+ return scenarios.find((scenario) => scenario.id === id || scenario.shortId === id || scenario.id.startsWith(id)) ?? null;
16977
+ }
16978
+ function resolveScenariosForRun(options) {
16377
16979
  if (options.scenarioIds && options.scenarioIds.length > 0) {
16378
- const all = listScenarios({ projectId: options.projectId });
16379
- scenarios = all.filter((s) => options.scenarioIds.includes(s.id) || options.scenarioIds.includes(s.shortId));
16380
- if (scenarios.length === 0 && options.projectId) {
16381
- const global2 = listScenarios({});
16382
- scenarios = global2.filter((s) => options.scenarioIds.includes(s.id) || options.scenarioIds.includes(s.shortId));
16980
+ const scoped = listScenarios({ projectId: options.projectId });
16981
+ const resolved = [];
16982
+ const seen = new Set;
16983
+ for (const id of options.scenarioIds) {
16984
+ const scenario = findScenarioInList(scoped, id) ?? getScenario(id);
16985
+ if (scenario && !seen.has(scenario.id)) {
16986
+ resolved.push(scenario);
16987
+ seen.add(scenario.id);
16988
+ }
16383
16989
  }
16384
- } else {
16385
- scenarios = listScenarios({
16386
- projectId: options.projectId,
16387
- tags: options.tags,
16388
- priority: options.priority
16389
- });
16990
+ return resolved;
16390
16991
  }
16992
+ return listScenarios({
16993
+ projectId: options.projectId,
16994
+ tags: options.tags,
16995
+ priority: options.priority
16996
+ });
16997
+ }
16998
+ async function runByFilter(options) {
16999
+ const scenarios = resolveScenariosForRun(options);
16391
17000
  if (scenarios.length === 0) {
16392
17001
  const config = loadConfig();
16393
17002
  const model = resolveModel2(options.model ?? config.defaultModel);
@@ -16400,17 +17009,7 @@ async function runByFilter(options) {
16400
17009
  function startRunAsync(options) {
16401
17010
  const config = loadConfig();
16402
17011
  const model = resolveModel2(options.model ?? config.defaultModel);
16403
- let scenarios;
16404
- if (options.scenarioIds && options.scenarioIds.length > 0) {
16405
- const all = listScenarios({ projectId: options.projectId });
16406
- scenarios = all.filter((s) => options.scenarioIds.includes(s.id) || options.scenarioIds.includes(s.shortId));
16407
- } else {
16408
- scenarios = listScenarios({
16409
- projectId: options.projectId,
16410
- tags: options.tags,
16411
- priority: options.priority
16412
- });
16413
- }
17012
+ const scenarios = resolveScenariosForRun(options);
16414
17013
  if (!options.skipBudgetCheck) {
16415
17014
  const cap = options.maxCostCents ?? config.defaultMaxCostCents;
16416
17015
  if (cap !== undefined && cap > 0 && scenarios.length > 0) {
@@ -16495,6 +17094,170 @@ function estimateCost(model, tokens) {
16495
17094
  const costPer1M = costs[model] ?? 0.5;
16496
17095
  return tokens / 1e6 * costPer1M * 100;
16497
17096
  }
17097
+ // src/lib/workflow-runner.ts
17098
+ init_database();
17099
+ import { mkdtempSync, rmSync, writeFileSync as writeFileSync3 } from "fs";
17100
+ import { tmpdir } from "os";
17101
+ import { join as join14 } from "path";
17102
+ function buildWorkflowRunPlan(workflow, options) {
17103
+ const runOptions = {
17104
+ url: options.url,
17105
+ model: options.model,
17106
+ headed: options.headed,
17107
+ parallel: options.parallel,
17108
+ timeout: options.timeout ?? workflow.execution.timeoutMs,
17109
+ projectId: workflow.projectId ?? undefined,
17110
+ scenarioIds: workflow.scenarioFilter.scenarioIds,
17111
+ tags: workflow.scenarioFilter.tags,
17112
+ priority: workflow.scenarioFilter.priority,
17113
+ personaIds: workflow.personaIds.length > 0 ? workflow.personaIds : undefined
17114
+ };
17115
+ return {
17116
+ workflow,
17117
+ runOptions,
17118
+ sandbox: workflow.execution.target === "sandbox" ? buildSandboxPlan(workflow, workflow.execution, runOptions) : null
17119
+ };
17120
+ }
17121
+ async function runTestingWorkflow(workflowId, options, dependencies = {}) {
17122
+ const workflow = getTestingWorkflow(workflowId);
17123
+ if (!workflow)
17124
+ throw new Error(`Testing workflow not found: ${workflowId}`);
17125
+ if (!workflow.enabled)
17126
+ throw new Error(`Testing workflow is disabled: ${workflow.name}`);
17127
+ validatePersonaIds(workflow);
17128
+ const plan = buildWorkflowRunPlan(workflow, options);
17129
+ if (options.dryRun)
17130
+ return { run: null, results: [], plan };
17131
+ if (workflow.execution.target === "sandbox") {
17132
+ const sandboxResult = await runViaSandbox(plan, dependencies);
17133
+ return { run: null, results: [], plan, sandboxResult };
17134
+ }
17135
+ const runLocal = dependencies.runByFilter ?? runByFilter;
17136
+ const { run, results } = await runLocal(plan.runOptions);
17137
+ return { run, results, plan };
17138
+ }
17139
+ function createWorkflowDatabaseBundle(workflow, plan) {
17140
+ if (!plan.sandbox)
17141
+ throw new Error(`Workflow is not configured for sandbox execution: ${workflow.name}`);
17142
+ const localDir = mkdtempSync(join14(tmpdir(), `testers-workflow-${workflow.id.slice(0, 8)}-`));
17143
+ writeFileSync3(join14(localDir, "testers.db"), getDatabase().serialize());
17144
+ return {
17145
+ localDir,
17146
+ remoteDir: plan.sandbox.stateRemoteDir,
17147
+ cleanup: () => rmSync(localDir, { recursive: true, force: true })
17148
+ };
17149
+ }
17150
+ function validatePersonaIds(workflow) {
17151
+ for (const personaId of workflow.personaIds) {
17152
+ if (!getPersona(personaId)) {
17153
+ throw new Error(`Persona not found for workflow ${workflow.name}: ${personaId}`);
17154
+ }
17155
+ }
17156
+ }
17157
+ function buildSandboxPlan(workflow, execution, runOptions) {
17158
+ const remoteDir = execution.sandboxRemoteDir ?? `/tmp/testers-workflow-${workflow.id.slice(0, 8)}`;
17159
+ const stateRemoteDir = `${remoteDir.replace(/\/+$/, "")}/.testers-state`;
17160
+ return {
17161
+ provider: execution.provider,
17162
+ image: execution.sandboxImage,
17163
+ name: `testers-${workflow.id.slice(0, 8)}`,
17164
+ remoteDir,
17165
+ stateRemoteDir,
17166
+ cleanup: execution.sandboxCleanup ?? "delete",
17167
+ timeoutMs: execution.timeoutMs,
17168
+ env: execution.env,
17169
+ command: buildSandboxCommand({
17170
+ runOptions,
17171
+ remoteDir,
17172
+ dbPath: `${stateRemoteDir}/testers.db`,
17173
+ setupCommand: execution.setupCommand,
17174
+ packageSpec: execution.packageSpec ?? "@hasna/testers"
17175
+ })
17176
+ };
17177
+ }
17178
+ function buildSandboxCommand(input) {
17179
+ const args = [
17180
+ "bunx",
17181
+ input.packageSpec,
17182
+ "run",
17183
+ input.runOptions.url,
17184
+ ...input.runOptions.scenarioIds?.length ? ["--scenario", input.runOptions.scenarioIds.join(",")] : [],
17185
+ ...input.runOptions.tags?.length ? input.runOptions.tags.flatMap((tag) => ["--tag", tag]) : [],
17186
+ ...input.runOptions.priority ? ["--priority", input.runOptions.priority] : [],
17187
+ ...input.runOptions.projectId ? ["--project", input.runOptions.projectId] : [],
17188
+ ...input.runOptions.model ? ["--model", input.runOptions.model] : [],
17189
+ ...input.runOptions.headed ? ["--headed"] : [],
17190
+ ...input.runOptions.parallel ? ["--parallel", String(input.runOptions.parallel)] : [],
17191
+ ...input.runOptions.timeout ? ["--timeout", String(input.runOptions.timeout)] : [],
17192
+ ...input.runOptions.personaIds?.length ? ["--persona", input.runOptions.personaIds.join(",")] : [],
17193
+ "--no-auto-generate",
17194
+ "--json"
17195
+ ];
17196
+ return [
17197
+ "set -euo pipefail",
17198
+ `mkdir -p ${shellQuote(input.remoteDir)}`,
17199
+ `cd ${shellQuote(input.remoteDir)}`,
17200
+ input.setupCommand,
17201
+ `HASNA_TESTERS_DB_PATH=${shellQuote(input.dbPath)} ${args.map(shellQuote).join(" ")}`
17202
+ ].filter(Boolean).join(`
17203
+ `);
17204
+ }
17205
+ async function runViaSandbox(plan, dependencies) {
17206
+ if (!plan.sandbox)
17207
+ throw new Error("Workflow does not have a sandbox plan");
17208
+ const sandboxes = await resolveSandboxesRuntime(dependencies);
17209
+ const createBundle = dependencies.createDatabaseBundle ?? createWorkflowDatabaseBundle;
17210
+ const bundle = createBundle(plan.workflow, plan);
17211
+ try {
17212
+ const raw = await sandboxes.runCommandInSandbox({
17213
+ command: plan.sandbox.command,
17214
+ provider: plan.sandbox.provider,
17215
+ name: plan.sandbox.name,
17216
+ image: plan.sandbox.image,
17217
+ sandboxTimeout: plan.sandbox.timeoutMs,
17218
+ commandTimeoutMs: plan.sandbox.timeoutMs,
17219
+ projectId: plan.workflow.projectId ?? undefined,
17220
+ config: {
17221
+ source: "testers",
17222
+ workflowId: plan.workflow.id,
17223
+ workflowName: plan.workflow.name
17224
+ },
17225
+ sandboxEnvVars: plan.sandbox.env,
17226
+ cleanup: plan.sandbox.cleanup,
17227
+ upload: {
17228
+ localDir: bundle.localDir,
17229
+ remoteDir: bundle.remoteDir
17230
+ }
17231
+ });
17232
+ const exitCode = raw.result.exit_code ?? raw.result.exitCode ?? 0;
17233
+ const stdout = raw.result.stdout ?? "";
17234
+ const stderr = raw.result.stderr ?? "";
17235
+ if (exitCode !== 0) {
17236
+ throw new Error(`Sandbox workflow execution failed (${exitCode}): ${stderr || stdout}`);
17237
+ }
17238
+ return {
17239
+ sandboxId: raw.sandbox.id,
17240
+ sessionId: raw.session.id,
17241
+ exitCode,
17242
+ stdout,
17243
+ stderr,
17244
+ cleanup: raw.cleanup
17245
+ };
17246
+ } finally {
17247
+ bundle.cleanup?.();
17248
+ }
17249
+ }
17250
+ async function resolveSandboxesRuntime(dependencies) {
17251
+ if (dependencies.sandboxes)
17252
+ return dependencies.sandboxes;
17253
+ if (dependencies.createSandboxesSDK)
17254
+ return dependencies.createSandboxesSDK();
17255
+ const mod = await import("@hasna/sandboxes");
17256
+ return mod.createSandboxesSDK();
17257
+ }
17258
+ function shellQuote(value) {
17259
+ return `'${value.replaceAll("'", `'"'"'`)}'`;
17260
+ }
16498
17261
  // src/lib/reporter.ts
16499
17262
  init_database();
16500
17263
  function useEmoji() {
@@ -16668,9 +17431,9 @@ function formatRunList(runs) {
16668
17431
  `);
16669
17432
  }
16670
17433
  function getScenarioRunStats(scenarioId) {
16671
- const db2 = getDatabase();
16672
- const lastRow = db2.query("SELECT status FROM results WHERE scenario_id = ? ORDER BY created_at DESC LIMIT 1").get(scenarioId);
16673
- const statsRow = db2.query("SELECT COUNT(*) as total, SUM(CASE WHEN status = 'passed' THEN 1 ELSE 0 END) as passed FROM results WHERE scenario_id = ?").get(scenarioId);
17434
+ const db3 = getDatabase();
17435
+ const lastRow = db3.query("SELECT status FROM results WHERE scenario_id = ? ORDER BY created_at DESC LIMIT 1").get(scenarioId);
17436
+ const statsRow = db3.query("SELECT COUNT(*) as total, SUM(CASE WHEN status = 'passed' THEN 1 ELSE 0 END) as passed FROM results WHERE scenario_id = ?").get(scenarioId);
16674
17437
  return {
16675
17438
  lastStatus: lastRow ? lastRow.status : null,
16676
17439
  passRate: statsRow && statsRow.total > 0 ? `${statsRow.passed}/${statsRow.total}` : "\u2014"
@@ -16960,10 +17723,10 @@ class Scheduler {
16960
17723
  }
16961
17724
  // src/lib/init.ts
16962
17725
  init_paths();
16963
- import { existsSync as existsSync11, readFileSync as readFileSync3, writeFileSync as writeFileSync3, mkdirSync as mkdirSync9 } from "fs";
16964
- import { join as join14, basename } from "path";
17726
+ import { existsSync as existsSync11, readFileSync as readFileSync3, writeFileSync as writeFileSync4, mkdirSync as mkdirSync9 } from "fs";
17727
+ import { join as join15, basename } from "path";
16965
17728
  function detectFramework(dir) {
16966
- const pkgPath = join14(dir, "package.json");
17729
+ const pkgPath = join15(dir, "package.json");
16967
17730
  if (!existsSync11(pkgPath))
16968
17731
  return null;
16969
17732
  let pkg;
@@ -17191,7 +17954,7 @@ function initProject(options) {
17191
17954
  }
17192
17955
  }).filter((s) => s !== null);
17193
17956
  const configDir = getTestersDir();
17194
- const configPath = join14(configDir, "config.json");
17957
+ const configPath = join15(configDir, "config.json");
17195
17958
  if (!existsSync11(configDir)) {
17196
17959
  mkdirSync9(configDir, { recursive: true });
17197
17960
  }
@@ -17202,7 +17965,7 @@ function initProject(options) {
17202
17965
  } catch {}
17203
17966
  }
17204
17967
  config.activeProject = project.id;
17205
- writeFileSync3(configPath, JSON.stringify(config, null, 2), "utf-8");
17968
+ writeFileSync4(configPath, JSON.stringify(config, null, 2), "utf-8");
17206
17969
  return { project, scenarios, framework, url };
17207
17970
  }
17208
17971
  // src/lib/smoke.ts
@@ -17630,28 +18393,28 @@ function fromRow2(row) {
17630
18393
  };
17631
18394
  }
17632
18395
  function createAuthPreset(input) {
17633
- const db2 = getDatabase();
18396
+ const db3 = getDatabase();
17634
18397
  const id = uuid();
17635
18398
  const timestamp = now();
17636
- db2.query(`
18399
+ db3.query(`
17637
18400
  INSERT INTO auth_presets (id, name, email, password, login_path, metadata, created_at)
17638
18401
  VALUES (?, ?, ?, ?, ?, '{}', ?)
17639
18402
  `).run(id, input.name, input.email, input.password, input.loginPath ?? "/login", timestamp);
17640
18403
  return getAuthPreset(input.name);
17641
18404
  }
17642
18405
  function getAuthPreset(name) {
17643
- const db2 = getDatabase();
17644
- const row = db2.query("SELECT * FROM auth_presets WHERE name = ?").get(name);
18406
+ const db3 = getDatabase();
18407
+ const row = db3.query("SELECT * FROM auth_presets WHERE name = ?").get(name);
17645
18408
  return row ? fromRow2(row) : null;
17646
18409
  }
17647
18410
  function listAuthPresets() {
17648
- const db2 = getDatabase();
17649
- const rows = db2.query("SELECT * FROM auth_presets ORDER BY created_at DESC").all();
18411
+ const db3 = getDatabase();
18412
+ const rows = db3.query("SELECT * FROM auth_presets ORDER BY created_at DESC").all();
17650
18413
  return rows.map(fromRow2);
17651
18414
  }
17652
18415
  function deleteAuthPreset(name) {
17653
- const db2 = getDatabase();
17654
- const result = db2.query("DELETE FROM auth_presets WHERE name = ?").run(name);
18416
+ const db3 = getDatabase();
18417
+ const result = db3.query("DELETE FROM auth_presets WHERE name = ?").run(name);
17655
18418
  return result.changes > 0;
17656
18419
  }
17657
18420
  // src/lib/report.ts
@@ -17947,12 +18710,12 @@ async function startWatcher(options) {
17947
18710
  }
17948
18711
  // src/lib/repo-discovery.ts
17949
18712
  init_paths();
17950
- import { existsSync as existsSync13, readFileSync as readFileSync5, readdirSync as readdirSync3, statSync, writeFileSync as writeFileSync4, mkdirSync as mkdirSync10, unlinkSync } from "fs";
18713
+ import { existsSync as existsSync13, readFileSync as readFileSync5, readdirSync as readdirSync3, statSync, writeFileSync as writeFileSync5, mkdirSync as mkdirSync10, unlinkSync } from "fs";
17951
18714
  import { createHash } from "crypto";
17952
- import { join as join15, resolve as resolve2, relative as relative2 } from "path";
18715
+ import { join as join16, resolve as resolve2, relative as relative2 } from "path";
17953
18716
  function getCacheDir() {
17954
18717
  const testersDir = getTestersDir();
17955
- const cacheDir = join15(testersDir, "repo-index");
18718
+ const cacheDir = join16(testersDir, "repo-index");
17956
18719
  if (!existsSync13(cacheDir)) {
17957
18720
  mkdirSync10(cacheDir, { recursive: true });
17958
18721
  }
@@ -17962,11 +18725,11 @@ function pathHash(repoPath) {
17962
18725
  return createHash("sha256").update(repoPath).digest("hex").slice(0, 16);
17963
18726
  }
17964
18727
  function getCachePath(repoPath) {
17965
- return join15(getCacheDir(), `${pathHash(repoPath)}.json`);
18728
+ return join16(getCacheDir(), `${pathHash(repoPath)}.json`);
17966
18729
  }
17967
18730
  function isCacheStale(cached, repoPath) {
17968
18731
  for (const spec of cached.specs) {
17969
- const fullPath = join15(repoPath, spec.file);
18732
+ const fullPath = join16(repoPath, spec.file);
17970
18733
  if (!existsSync13(fullPath))
17971
18734
  return true;
17972
18735
  try {
@@ -17978,11 +18741,11 @@ function isCacheStale(cached, repoPath) {
17978
18741
  }
17979
18742
  }
17980
18743
  if (cached.configPath) {
17981
- const configFullPath = join15(repoPath, cached.configPath);
18744
+ const configFullPath = join16(repoPath, cached.configPath);
17982
18745
  if (!existsSync13(configFullPath))
17983
18746
  return true;
17984
18747
  try {
17985
- const stat = statSync(configFullPath);
18748
+ statSync(configFullPath);
17986
18749
  const age = Date.now() - new Date(cached.snapshotAt).getTime();
17987
18750
  if (age > 3600000)
17988
18751
  return true;
@@ -18005,14 +18768,14 @@ function loadCache(repoPath) {
18005
18768
  }
18006
18769
  function saveCache(snapshot) {
18007
18770
  const cachePath = getCachePath(snapshot.repoPath);
18008
- writeFileSync4(cachePath, JSON.stringify(snapshot, null, 2), "utf-8");
18771
+ writeFileSync5(cachePath, JSON.stringify(snapshot, null, 2), "utf-8");
18009
18772
  }
18010
18773
  function detectPackageManager(repoPath) {
18011
18774
  const result = {
18012
- npm: existsSync13(join15(repoPath, "package-lock.json")),
18013
- yarn: existsSync13(join15(repoPath, "yarn.lock")),
18014
- pnpm: existsSync13(join15(repoPath, "pnpm-lock.yaml")),
18015
- bun: existsSync13(join15(repoPath, "bun.lockb")) || existsSync13(join15(repoPath, "bun.lock")),
18775
+ npm: existsSync13(join16(repoPath, "package-lock.json")),
18776
+ yarn: existsSync13(join16(repoPath, "yarn.lock")),
18777
+ pnpm: existsSync13(join16(repoPath, "pnpm-lock.yaml")),
18778
+ bun: existsSync13(join16(repoPath, "bun.lockb")) || existsSync13(join16(repoPath, "bun.lock")),
18016
18779
  preferred: "npm"
18017
18780
  };
18018
18781
  if (result.bun)
@@ -18026,7 +18789,7 @@ function detectPackageManager(repoPath) {
18026
18789
  return result;
18027
18790
  }
18028
18791
  function detectDevScripts(repoPath) {
18029
- const pkgPath = join15(repoPath, "package.json");
18792
+ const pkgPath = join16(repoPath, "package.json");
18030
18793
  if (!existsSync13(pkgPath)) {
18031
18794
  return { dev: null, test: null, seed: null, build: null };
18032
18795
  }
@@ -18053,7 +18816,7 @@ function findPlaywrightConfig(repoPath) {
18053
18816
  "playwright-ct.config.js"
18054
18817
  ];
18055
18818
  for (const name of candidates) {
18056
- if (existsSync13(join15(repoPath, name)))
18819
+ if (existsSync13(join16(repoPath, name)))
18057
18820
  return name;
18058
18821
  }
18059
18822
  return null;
@@ -18062,7 +18825,7 @@ function extractTestGlobPatterns(configPath, repoPath) {
18062
18825
  if (!configPath) {
18063
18826
  return ["**/*.spec.ts", "**/*.spec.js", "**/*.test.ts", "**/*.test.js", "**/e2e/**/*.ts", "**/e2e/**/*.js", "**/tests/**/*.ts", "**/tests/**/*.js"];
18064
18827
  }
18065
- const fullPath = join15(repoPath, configPath);
18828
+ const fullPath = join16(repoPath, configPath);
18066
18829
  let content;
18067
18830
  try {
18068
18831
  content = readFileSync5(fullPath, "utf-8");
@@ -18073,8 +18836,9 @@ function extractTestGlobPatterns(configPath, repoPath) {
18073
18836
  const testDirMatch = content.match(/testDir\s*[:=]\s*['"`]([^'"`]+)['"`]/);
18074
18837
  const testDir = testDirMatch?.[1];
18075
18838
  const testMatchArray = content.match(/testMatch\s*[:=]\s*\[([^\]]+)\]/);
18076
- if (testMatchArray) {
18077
- const items = testMatchArray[1].match(/['"`]([^'"`]+)['"`]/g);
18839
+ const testMatchBody = testMatchArray?.[1];
18840
+ if (testMatchBody) {
18841
+ const items = testMatchBody.match(/['"`]([^'"`]+)['"`]/g);
18078
18842
  if (items) {
18079
18843
  for (const item of items) {
18080
18844
  patterns.push(item.replace(/['"`]/g, ""));
@@ -18082,8 +18846,9 @@ function extractTestGlobPatterns(configPath, repoPath) {
18082
18846
  }
18083
18847
  }
18084
18848
  const testMatchSingle = content.match(/testMatch\s*[:=]\s*['"`]([^'"`]+)['"`]/);
18085
- if (testMatchSingle) {
18086
- patterns.push(testMatchSingle[1]);
18849
+ const singleTestMatch = testMatchSingle?.[1];
18850
+ if (singleTestMatch) {
18851
+ patterns.push(singleTestMatch);
18087
18852
  }
18088
18853
  if (testDir && patterns.length === 0) {
18089
18854
  patterns.push(`${testDir}/**/*.spec.ts`, `${testDir}/**/*.test.ts`, `${testDir}/**/*.spec.js`, `${testDir}/**/*.test.js`);
@@ -18109,7 +18874,7 @@ function findSpecFiles(repoPath, globPatterns) {
18109
18874
  for (const pattern of globPatterns) {
18110
18875
  const dirsToSearch = ["", ".", "tests", "e2e", "test", "__tests__", "specs", "src"];
18111
18876
  for (const dir of dirsToSearch) {
18112
- const searchDir = dir ? join15(repoPath, dir) : repoPath;
18877
+ const searchDir = dir ? join16(repoPath, dir) : repoPath;
18113
18878
  if (!existsSync13(searchDir))
18114
18879
  continue;
18115
18880
  try {
@@ -18143,7 +18908,7 @@ function walkDir(dir) {
18143
18908
  try {
18144
18909
  const entries = readdirSync3(dir, { withFileTypes: true });
18145
18910
  for (const entry of entries) {
18146
- const fullPath = join15(dir, entry.name);
18911
+ const fullPath = join16(dir, entry.name);
18147
18912
  if (entry.isDirectory()) {
18148
18913
  if (entry.name === "node_modules" || entry.name === ".git")
18149
18914
  continue;
@@ -18161,7 +18926,7 @@ function matchesGlob(filePath, pattern) {
18161
18926
  return new RegExp(regex).test(filePath);
18162
18927
  }
18163
18928
  function detectSuggestedUrl(repoPath) {
18164
- const pkgPath = join15(repoPath, "package.json");
18929
+ const pkgPath = join16(repoPath, "package.json");
18165
18930
  if (!existsSync13(pkgPath))
18166
18931
  return null;
18167
18932
  try {
@@ -18181,10 +18946,10 @@ function detectSuggestedUrl(repoPath) {
18181
18946
  return null;
18182
18947
  }
18183
18948
  function checkPlaywrightBrowserInstalled(repoPath) {
18184
- const cacheDir = join15(repoPath, "node_modules", ".cache", "ms-playwright");
18949
+ const cacheDir = join16(repoPath, "node_modules", ".cache", "ms-playwright");
18185
18950
  if (existsSync13(cacheDir))
18186
18951
  return true;
18187
- const globalCache = join15(repoPath, ".cache", "ms-playwright");
18952
+ const globalCache = join16(repoPath, ".cache", "ms-playwright");
18188
18953
  if (existsSync13(globalCache))
18189
18954
  return true;
18190
18955
  return false;
@@ -18201,7 +18966,7 @@ function getInstallCommand(pm) {
18201
18966
  return "bun install";
18202
18967
  }
18203
18968
  }
18204
- function getPlaywrightInstallCommand(pm) {
18969
+ function getPlaywrightInstallCommand(_pm) {
18205
18970
  return "npx playwright install";
18206
18971
  }
18207
18972
  function discoverRepo(opts) {
@@ -18216,7 +18981,7 @@ function discoverRepo(opts) {
18216
18981
  let configRaw = null;
18217
18982
  if (configPath) {
18218
18983
  try {
18219
- configRaw = readFileSync5(join15(repoPath, configPath), "utf-8");
18984
+ configRaw = readFileSync5(join16(repoPath, configPath), "utf-8");
18220
18985
  } catch {
18221
18986
  configRaw = null;
18222
18987
  }
@@ -18225,7 +18990,7 @@ function discoverRepo(opts) {
18225
18990
  const specs = findSpecFiles(repoPath, globPatterns);
18226
18991
  const packageManager = detectPackageManager(repoPath);
18227
18992
  const devScripts = detectDevScripts(repoPath);
18228
- const playwrightInstalled = existsSync13(join15(repoPath, "node_modules", "playwright")) || existsSync13(join15(repoPath, "node_modules", "@playwright", "test"));
18993
+ const playwrightInstalled = existsSync13(join16(repoPath, "node_modules", "playwright")) || existsSync13(join16(repoPath, "node_modules", "@playwright", "test"));
18229
18994
  const browsersInstalled = checkPlaywrightBrowserInstalled(repoPath);
18230
18995
  const configExists = configPath !== null;
18231
18996
  const specsFound = specs.length > 0;
@@ -18294,7 +19059,7 @@ function clearDiscoveryCache(repoPath) {
18294
19059
  } else {
18295
19060
  for (const file of readdirSync3(cacheDir)) {
18296
19061
  if (file.endsWith(".json")) {
18297
- unlinkSync(join15(cacheDir, file));
19062
+ unlinkSync(join16(cacheDir, file));
18298
19063
  }
18299
19064
  }
18300
19065
  }
@@ -18317,10 +19082,10 @@ init_runs();
18317
19082
  init_database();
18318
19083
  init_paths();
18319
19084
  import { execSync as execSync2 } from "child_process";
18320
- import { existsSync as existsSync14, mkdirSync as mkdirSync11, writeFileSync as writeFileSync5 } from "fs";
18321
- import { join as join16 } from "path";
19085
+ import { existsSync as existsSync14, mkdirSync as mkdirSync11, writeFileSync as writeFileSync6 } from "fs";
19086
+ import { join as join17 } from "path";
18322
19087
  function resolvePlaywrightCmd(repoPath) {
18323
- const localPw = join16(repoPath, "node_modules", ".bin", "playwright");
19088
+ const localPw = join17(repoPath, "node_modules", ".bin", "playwright");
18324
19089
  if (existsSync14(localPw)) {
18325
19090
  return [localPw, "test"];
18326
19091
  }
@@ -18339,7 +19104,7 @@ function buildPlaywrightArgs(specFiles, extraArgs = []) {
18339
19104
  }
18340
19105
  function runPlaywright(repoPath, workingDir, specFiles, extraArgs, timeoutMs) {
18341
19106
  const cmd = resolvePlaywrightCmd(repoPath);
18342
- const args = buildPlaywrightArgs(specFiles, extraArgs, workingDir);
19107
+ const args = buildPlaywrightArgs(specFiles, extraArgs);
18343
19108
  const startTime = Date.now();
18344
19109
  try {
18345
19110
  const result = execSync2(`${cmd.join(" ")} ${args.join(" ")}`, {
@@ -18367,7 +19132,7 @@ function runPlaywright(repoPath, workingDir, specFiles, extraArgs, timeoutMs) {
18367
19132
  };
18368
19133
  }
18369
19134
  }
18370
- function parsePlaywrightJsonOutput(stdout, stderr) {
19135
+ function parsePlaywrightJsonOutput(stdout, _stderr) {
18371
19136
  const testResults = [];
18372
19137
  try {
18373
19138
  const obj = JSON.parse(stdout);
@@ -18472,19 +19237,21 @@ async function runRepoTests(opts) {
18472
19237
  const workingDir = opts.snapshot.workingDir;
18473
19238
  const repoPath = snapshot.repoPath;
18474
19239
  const url = opts.url ?? snapshot.suggestedUrl ?? "http://localhost:3000";
18475
- const run = createRun({
19240
+ const initialRun = createRun({
18476
19241
  projectId: opts.projectId,
18477
19242
  url,
18478
19243
  model: opts.model ?? "repo-native",
18479
19244
  headed: false,
18480
- parallel: 1,
18481
- metadata: {
19245
+ parallel: 1
19246
+ });
19247
+ const run = updateRun(initialRun.id, {
19248
+ metadata: JSON.stringify({
18482
19249
  runType: "repo-native",
18483
19250
  repoPath,
18484
19251
  configPath: snapshot.configPath,
18485
19252
  cacheKey: snapshot.cacheKey,
18486
19253
  label: opts.label
18487
- }
19254
+ })
18488
19255
  });
18489
19256
  const specResults = [];
18490
19257
  const startTime = Date.now();
@@ -18496,12 +19263,12 @@ async function runRepoTests(opts) {
18496
19263
  specResults.push(result);
18497
19264
  const resultId = uuid();
18498
19265
  const timestamp = now();
18499
- const db2 = getDatabase();
18500
- db2.exec("PRAGMA foreign_keys = OFF");
19266
+ const db3 = getDatabase();
19267
+ db3.exec("PRAGMA foreign_keys = OFF");
18501
19268
  try {
18502
19269
  const reasoning = result.status === "passed" ? "All tests passed" : (result.error ?? "").slice(0, 500) || null;
18503
19270
  const errorStr = result.status !== "passed" ? result.error ?? null : null;
18504
- db2.query(`
19271
+ db3.query(`
18505
19272
  INSERT INTO results (id, run_id, scenario_id, status, reasoning, error, steps_completed, steps_total, duration_ms, model, tokens_used, cost_cents, metadata, created_at, persona_id, persona_name)
18506
19273
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0, 0, ?, ?, NULL, NULL)
18507
19274
  `).run(resultId, run.id, "__repo__", result.status, reasoning, errorStr, result.testResults.filter((t) => t.status === "passed").length, result.testResults.length || 1, result.durationMs, "repo-native", JSON.stringify({
@@ -18510,14 +19277,14 @@ async function runRepoTests(opts) {
18510
19277
  testResults: result.testResults
18511
19278
  }), timestamp);
18512
19279
  } finally {
18513
- db2.exec("PRAGMA foreign_keys = ON");
19280
+ db3.exec("PRAGMA foreign_keys = ON");
18514
19281
  }
18515
19282
  const resultRecord = { id: resultId };
18516
19283
  if (result.stdout || result.stderr) {
18517
- const reportersDir = join16(getTestersDir(), "repo-run-output");
19284
+ const reportersDir = join17(getTestersDir(), "repo-run-output");
18518
19285
  mkdirSync11(reportersDir, { recursive: true });
18519
- const outputFile = join16(reportersDir, `${resultRecord.id}.log`);
18520
- writeFileSync5(outputFile, `=== stdout ===
19286
+ const outputFile = join17(reportersDir, `${resultRecord.id}.log`);
19287
+ writeFileSync6(outputFile, `=== stdout ===
18521
19288
  ${result.stdout}
18522
19289
 
18523
19290
  === stderr ===
@@ -19122,46 +19889,46 @@ async function postGitHubComment(run, results, options) {
19122
19889
  // src/db/sessions.ts
19123
19890
  init_database();
19124
19891
  function createSession(input) {
19125
- const db2 = getDatabase();
19892
+ const db3 = getDatabase();
19126
19893
  const id = input.sessionId ?? uuid();
19127
19894
  const timestamp = now();
19128
- db2.query(`
19895
+ db3.query(`
19129
19896
  INSERT INTO sessions (id, tab_id, url, title, entries, entry_count, error_count, console_count, nav_count, status, start_time, end_time, created_at)
19130
19897
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
19131
19898
  `).run(id, input.tabId, input.url ?? null, input.title ?? null, input.entries, input.entryCount, input.errorCount ?? 0, input.consoleCount ?? 0, input.navCount ?? 0, input.status, input.startTime, input.endTime ?? null, timestamp);
19132
19899
  return getSession(id);
19133
19900
  }
19134
19901
  function getSession(id) {
19135
- const db2 = getDatabase();
19136
- let row = db2.query("SELECT * FROM sessions WHERE id = ?").get(id);
19902
+ const db3 = getDatabase();
19903
+ let row = db3.query("SELECT * FROM sessions WHERE id = ?").get(id);
19137
19904
  if (row)
19138
19905
  return sessionFromRow(row);
19139
19906
  const fullId = resolvePartialId("sessions", id);
19140
19907
  if (fullId) {
19141
- row = db2.query("SELECT * FROM sessions WHERE id = ?").get(fullId);
19908
+ row = db3.query("SELECT * FROM sessions WHERE id = ?").get(fullId);
19142
19909
  if (row)
19143
19910
  return sessionFromRow(row);
19144
19911
  }
19145
19912
  return null;
19146
19913
  }
19147
19914
  function listSessions(limit = 50, offset = 0) {
19148
- const db2 = getDatabase();
19149
- const rows = db2.query("SELECT * FROM sessions ORDER BY created_at DESC LIMIT ? OFFSET ?").all(limit, offset);
19915
+ const db3 = getDatabase();
19916
+ const rows = db3.query("SELECT * FROM sessions ORDER BY created_at DESC LIMIT ? OFFSET ?").all(limit, offset);
19150
19917
  return rows.map(sessionFromRow);
19151
19918
  }
19152
19919
  function deleteSession(id) {
19153
- const db2 = getDatabase();
19154
- const result = db2.query("DELETE FROM sessions WHERE id = ?").run(id);
19920
+ const db3 = getDatabase();
19921
+ const result = db3.query("DELETE FROM sessions WHERE id = ?").run(id);
19155
19922
  return result.changes > 0;
19156
19923
  }
19157
19924
  function searchSessions(query, limit = 20) {
19158
- const db2 = getDatabase();
19159
- const rows = db2.query("SELECT * FROM sessions WHERE url LIKE ? OR title LIKE ? ORDER BY created_at DESC LIMIT ?").all(`%${query}%`, `%${query}%`, limit);
19925
+ const db3 = getDatabase();
19926
+ const rows = db3.query("SELECT * FROM sessions WHERE url LIKE ? OR title LIKE ? ORDER BY created_at DESC LIMIT ?").all(`%${query}%`, `%${query}%`, limit);
19160
19927
  return rows.map(sessionFromRow);
19161
19928
  }
19162
19929
  function countSessions() {
19163
- const db2 = getDatabase();
19164
- const row = db2.query("SELECT COUNT(*) as count FROM sessions").get();
19930
+ const db3 = getDatabase();
19931
+ const row = db3.query("SELECT COUNT(*) as count FROM sessions").get();
19165
19932
  return row.count;
19166
19933
  }
19167
19934
  function sessionFromRow(row) {
@@ -19185,6 +19952,7 @@ export {
19185
19952
  writeScenarioMeta,
19186
19953
  writeRunMeta,
19187
19954
  uuid,
19955
+ updateTestingWorkflow,
19188
19956
  updateSchedule,
19189
19957
  updateScenario,
19190
19958
  updateRun,
@@ -19202,6 +19970,7 @@ export {
19202
19970
  screenshotFromRow,
19203
19971
  scheduleFromRow,
19204
19972
  scenarioFromRow,
19973
+ runTestingWorkflow,
19205
19974
  runSmoke,
19206
19975
  runSingleScenario,
19207
19976
  runRepoTests,
@@ -19233,6 +20002,7 @@ export {
19233
20002
  loginWithAuthConfig,
19234
20003
  loadConfig,
19235
20004
  listWebhooks,
20005
+ listTestingWorkflows,
19236
20006
  listTemplateNames,
19237
20007
  listSessions,
19238
20008
  listScreenshots,
@@ -19255,6 +20025,7 @@ export {
19255
20025
  imageToBase64,
19256
20026
  getWebhook,
19257
20027
  getTransitiveDependencies,
20028
+ getTestingWorkflow,
19258
20029
  getTemplate,
19259
20030
  getStarterScenarios,
19260
20031
  getSession,
@@ -19311,13 +20082,16 @@ export {
19311
20082
  diffRuns,
19312
20083
  detectFramework,
19313
20084
  deleteWebhook,
20085
+ deleteTestingWorkflow,
19314
20086
  deleteSession,
19315
20087
  deleteSchedule,
19316
20088
  deleteScenario,
19317
20089
  deleteRun,
19318
20090
  deleteFlow,
19319
20091
  deleteAuthPreset,
20092
+ createWorkflowDatabaseBundle,
19320
20093
  createWebhook,
20094
+ createTestingWorkflow,
19321
20095
  createSession,
19322
20096
  createScreenshot,
19323
20097
  createSchedule,
@@ -19336,6 +20110,7 @@ export {
19336
20110
  closeBrowser,
19337
20111
  clearDiscoveryCache,
19338
20112
  checkBudget,
20113
+ buildWorkflowRunPlan,
19339
20114
  agentFromRow,
19340
20115
  addDependency,
19341
20116
  VersionConflictError,