@hasna/testers 0.0.33 → 0.0.35

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli/index.js CHANGED
@@ -2100,6 +2100,56 @@ var require_commander = __commonJS((exports) => {
2100
2100
  });
2101
2101
 
2102
2102
  // src/types/index.ts
2103
+ function isRecord(value) {
2104
+ return typeof value === "object" && value !== null && !Array.isArray(value);
2105
+ }
2106
+ function stringValue(value) {
2107
+ return typeof value === "string" && value.trim() ? value : undefined;
2108
+ }
2109
+ function numberValue(value) {
2110
+ return typeof value === "number" && Number.isFinite(value) ? value : undefined;
2111
+ }
2112
+ function stringMap(value) {
2113
+ if (!isRecord(value))
2114
+ return;
2115
+ const entries = Object.entries(value).filter((entry) => typeof entry[1] === "string");
2116
+ return entries.length > 0 ? Object.fromEntries(entries) : undefined;
2117
+ }
2118
+ function cleanupValue(value) {
2119
+ if (value === "delete" || value === "stop" || value === "keep")
2120
+ return value;
2121
+ return;
2122
+ }
2123
+ function workflowExecutionFromValue(value) {
2124
+ const input = isRecord(value) ? value : {};
2125
+ const rawTarget = stringValue(input["target"]) ?? "local";
2126
+ if (rawTarget === "local") {
2127
+ const timeoutMs2 = numberValue(input["timeoutMs"]);
2128
+ return timeoutMs2 === undefined ? { target: "local" } : { target: "local", timeoutMs: timeoutMs2 };
2129
+ }
2130
+ if (rawTarget !== "sandbox" && rawTarget !== "connector:e2b") {
2131
+ throw new Error(`Unsupported workflow execution target: ${rawTarget}`);
2132
+ }
2133
+ const provider = rawTarget === "connector:e2b" ? "e2b" : stringValue(input["provider"]) ?? stringValue(input["connector"]);
2134
+ const sandboxImage = stringValue(input["sandboxImage"]) ?? stringValue(input["sandboxTemplate"]);
2135
+ const sandboxRemoteDir = stringValue(input["sandboxRemoteDir"]);
2136
+ const sandboxCleanup = cleanupValue(input["sandboxCleanup"]);
2137
+ const setupCommand = stringValue(input["setupCommand"]);
2138
+ const packageSpec = stringValue(input["packageSpec"]);
2139
+ const timeoutMs = numberValue(input["timeoutMs"]);
2140
+ const env = stringMap(input["env"]);
2141
+ return {
2142
+ target: "sandbox",
2143
+ ...provider ? { provider } : {},
2144
+ ...sandboxImage ? { sandboxImage } : {},
2145
+ ...sandboxRemoteDir ? { sandboxRemoteDir } : {},
2146
+ ...sandboxCleanup ? { sandboxCleanup } : {},
2147
+ ...setupCommand ? { setupCommand } : {},
2148
+ ...packageSpec ? { packageSpec } : {},
2149
+ ...timeoutMs !== undefined ? { timeoutMs } : {},
2150
+ ...env ? { env } : {}
2151
+ };
2152
+ }
2103
2153
  function workflowFromRow(row) {
2104
2154
  return {
2105
2155
  id: row.id,
@@ -2109,7 +2159,7 @@ function workflowFromRow(row) {
2109
2159
  scenarioFilter: JSON.parse(row.scenario_filter || "{}"),
2110
2160
  personaIds: JSON.parse(row.persona_ids || "[]"),
2111
2161
  goal: row.goal ? JSON.parse(row.goal) : null,
2112
- execution: JSON.parse(row.execution || '{"target":"local"}'),
2162
+ execution: workflowExecutionFromValue(JSON.parse(row.execution || '{"target":"local"}')),
2113
2163
  settings: JSON.parse(row.settings || "{}"),
2114
2164
  enabled: row.enabled === 1,
2115
2165
  createdAt: row.created_at,
@@ -14074,6 +14124,7 @@ __export(exports_ai_client, {
14074
14124
  createClientForModel: () => createClientForModel,
14075
14125
  createClient: () => createClient,
14076
14126
  callOpenAICompatible: () => callOpenAICompatible,
14127
+ buildScenarioUserMessage: () => buildScenarioUserMessage,
14077
14128
  BROWSER_TOOLS: () => BROWSER_TOOLS
14078
14129
  });
14079
14130
  import Anthropic2 from "@anthropic-ai/sdk";
@@ -14486,7 +14537,6 @@ async function executeTool(page, screenshotter, toolName, toolInput, context) {
14486
14537
  const assertionType = toolInput.assertion_type;
14487
14538
  const selector = toolInput.selector;
14488
14539
  const expected = toolInput.expected;
14489
- const sessionId = context.sessionId ?? "default";
14490
14540
  switch (assertionType) {
14491
14541
  case "element_exists": {
14492
14542
  if (!selector)
@@ -14551,7 +14601,6 @@ async function executeTool(page, screenshotter, toolName, toolInput, context) {
14551
14601
  case "browser_intercept": {
14552
14602
  const action = toolInput.action;
14553
14603
  const pattern = toolInput.pattern;
14554
- const interceptAction = toolInput.intercept_action;
14555
14604
  const statusCode = toolInput.status_code;
14556
14605
  const body = toolInput.body;
14557
14606
  const sessionId = context.sessionId ?? "default";
@@ -14628,7 +14677,28 @@ ${JSON.stringify(har, null, 2)}` };
14628
14677
  }
14629
14678
  case "browser_a11y": {
14630
14679
  const level = toolInput.level ?? "AA";
14631
- const snapshot = await page.accessibility.snapshot();
14680
+ const snapshot = await page.evaluate(() => {
14681
+ function readRole(el) {
14682
+ return el.getAttribute("role") ?? el.tagName.toLowerCase();
14683
+ }
14684
+ function readName(el) {
14685
+ const labelledBy = el.getAttribute("aria-labelledby");
14686
+ if (labelledBy) {
14687
+ const labelledText = labelledBy.split(/\s+/).map((id) => document.getElementById(id)?.textContent?.trim()).filter(Boolean).join(" ");
14688
+ if (labelledText)
14689
+ return labelledText;
14690
+ }
14691
+ return el.getAttribute("aria-label") ?? el.getAttribute("alt") ?? el.textContent?.trim() ?? "";
14692
+ }
14693
+ function walk(el) {
14694
+ return {
14695
+ role: readRole(el),
14696
+ name: readName(el),
14697
+ children: Array.from(el.children).map((child) => walk(child))
14698
+ };
14699
+ }
14700
+ return document.body ? walk(document.body) : null;
14701
+ });
14632
14702
  if (!snapshot)
14633
14703
  return { result: "Error: could not capture accessibility tree" };
14634
14704
  const issues = [];
@@ -14670,6 +14740,38 @@ ${filtered.join(`
14670
14740
  return { result: `Error executing ${toolName}: ${message}` };
14671
14741
  }
14672
14742
  }
14743
+ function resolveStartUrl(baseUrl, targetPath) {
14744
+ try {
14745
+ return new URL(targetPath, baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`).toString();
14746
+ } catch {
14747
+ return `${baseUrl.replace(/\/+$/, "")}/${targetPath.replace(/^\/+/, "")}`;
14748
+ }
14749
+ }
14750
+ function buildScenarioUserMessage(scenario, baseUrl) {
14751
+ const userParts = [
14752
+ `**Scenario:** ${scenario.name}`,
14753
+ `**Description:** ${scenario.description}`
14754
+ ];
14755
+ if (baseUrl) {
14756
+ const normalizedBaseUrl = baseUrl.replace(/\/+$/, "");
14757
+ userParts.push(`**Base URL:** ${normalizedBaseUrl}`);
14758
+ if (scenario.targetPath) {
14759
+ userParts.push(`**Start URL:** ${resolveStartUrl(normalizedBaseUrl, scenario.targetPath)}`);
14760
+ }
14761
+ 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.");
14762
+ }
14763
+ if (scenario.targetPath) {
14764
+ userParts.push(`**Target Path:** ${scenario.targetPath}`);
14765
+ }
14766
+ if (scenario.steps.length > 0) {
14767
+ userParts.push("**Steps:**");
14768
+ for (let i = 0;i < scenario.steps.length; i++) {
14769
+ userParts.push(`${i + 1}. ${scenario.steps[i]}`);
14770
+ }
14771
+ }
14772
+ return userParts.join(`
14773
+ `);
14774
+ }
14673
14775
  async function runAgentLoop(options) {
14674
14776
  const {
14675
14777
  client,
@@ -14679,6 +14781,7 @@ async function runAgentLoop(options) {
14679
14781
  model,
14680
14782
  runId,
14681
14783
  sessionId,
14784
+ baseUrl,
14682
14785
  maxTurns = 30,
14683
14786
  onStep,
14684
14787
  persona,
@@ -14726,21 +14829,7 @@ Instructions: ${persona.instructions}` : "",
14726
14829
  "- Verify both positive and negative states"
14727
14830
  ].join(`
14728
14831
  `) + personaSection;
14729
- const userParts = [
14730
- `**Scenario:** ${scenario.name}`,
14731
- `**Description:** ${scenario.description}`
14732
- ];
14733
- if (scenario.targetPath) {
14734
- userParts.push(`**Target Path:** ${scenario.targetPath}`);
14735
- }
14736
- if (scenario.steps.length > 0) {
14737
- userParts.push("**Steps:**");
14738
- for (let i = 0;i < scenario.steps.length; i++) {
14739
- userParts.push(`${i + 1}. ${scenario.steps[i]}`);
14740
- }
14741
- }
14742
- const userMessage = userParts.join(`
14743
- `);
14832
+ const userMessage = buildScenarioUserMessage(scenario, baseUrl);
14744
14833
  const screenshots = [];
14745
14834
  let tokensUsed = 0;
14746
14835
  let stepNumber = 0;
@@ -14803,7 +14892,7 @@ Instructions: ${persona.instructions}` : "",
14803
14892
  if (onStep) {
14804
14893
  onStep({ type: "tool_call", toolName: toolBlock.name, toolInput, stepNumber });
14805
14894
  }
14806
- const execResult = await executeTool(page, screenshotter, toolBlock.name, toolInput, { runId, scenarioSlug, stepNumber, sessionId, a11y });
14895
+ const execResult = await executeTool(page, screenshotter, toolBlock.name, toolInput, { runId, scenarioSlug, stepNumber, sessionId: sessionId ?? runId, a11y });
14807
14896
  if (onStep) {
14808
14897
  onStep({ type: "tool_result", toolName: toolBlock.name, toolResult: execResult.result, stepNumber });
14809
14898
  }
@@ -17494,6 +17583,381 @@ var init_failure_pipeline = __esm(() => {
17494
17583
  init_todos_connector();
17495
17584
  });
17496
17585
 
17586
+ // src/lib/a11y-audit.ts
17587
+ async function runA11yAudit(page, options = {}) {
17588
+ const { level = "AA", rules, exclude = [] } = options;
17589
+ await page.addScriptTag({ url: "https://cdnjs.cloudflare.com/ajax/libs/axe-core/4.9.1/axe.min.js" });
17590
+ const config = {
17591
+ runOnly: {
17592
+ type: level === "AAA" ? "standard" : "tag",
17593
+ values: level === "AAA" ? undefined : [level, "best-practice"]
17594
+ }
17595
+ };
17596
+ if (rules && rules.length > 0) {
17597
+ config.rules = Object.fromEntries(rules.map((r) => [r, { enabled: true }]));
17598
+ }
17599
+ if (exclude.length > 0) {
17600
+ config.exclude = exclude;
17601
+ }
17602
+ const result = await page.evaluate(async (auditConfig) => {
17603
+ const axeResult = await window.axe.run(auditConfig);
17604
+ return axeResult;
17605
+ }, config);
17606
+ const violations = (result.violations ?? []).map((v) => ({
17607
+ id: v.id,
17608
+ impact: v.impact,
17609
+ description: v.description,
17610
+ help: v.help,
17611
+ helpUrl: v.helpUrl,
17612
+ nodes: (v.nodes ?? []).map((n) => ({
17613
+ html: n.html,
17614
+ target: n.target,
17615
+ failureSummary: n.failureSummary
17616
+ }))
17617
+ }));
17618
+ const passes = (result.passes ?? []).map((p) => ({
17619
+ id: p.id,
17620
+ description: p.description
17621
+ }));
17622
+ const incomplete = (result.incomplete ?? []).map((i) => ({
17623
+ id: i.id,
17624
+ description: i.description,
17625
+ impact: i.impact
17626
+ }));
17627
+ const criticalCount = violations.filter((v) => v.impact === "critical").length;
17628
+ const seriousCount = violations.filter((v) => v.impact === "serious").length;
17629
+ const moderateCount = violations.filter((v) => v.impact === "moderate").length;
17630
+ const minorCount = violations.filter((v) => v.impact === "minor").length;
17631
+ return {
17632
+ violations,
17633
+ passes,
17634
+ incomplete,
17635
+ url: page.url(),
17636
+ timestamp: new Date().toISOString(),
17637
+ totalViolations: violations.length,
17638
+ criticalCount,
17639
+ seriousCount,
17640
+ moderateCount,
17641
+ minorCount
17642
+ };
17643
+ }
17644
+
17645
+ // src/lib/assertions.ts
17646
+ async function evaluateAssertions(page, assertions, context = {}) {
17647
+ const results = [];
17648
+ for (const assertion of assertions) {
17649
+ try {
17650
+ const result = await evaluateOne(page, assertion, context);
17651
+ results.push(result);
17652
+ } catch (err) {
17653
+ results.push({
17654
+ assertion,
17655
+ passed: false,
17656
+ actual: "",
17657
+ error: err instanceof Error ? err.message : String(err)
17658
+ });
17659
+ }
17660
+ }
17661
+ return results;
17662
+ }
17663
+ async function evaluateOne(page, assertion, context) {
17664
+ switch (assertion.type) {
17665
+ case "visible": {
17666
+ const visible = await page.locator(assertion.selector).isVisible();
17667
+ return {
17668
+ assertion,
17669
+ passed: visible,
17670
+ actual: String(visible)
17671
+ };
17672
+ }
17673
+ case "not_visible": {
17674
+ const visible = await page.locator(assertion.selector).isVisible();
17675
+ return {
17676
+ assertion,
17677
+ passed: !visible,
17678
+ actual: String(visible)
17679
+ };
17680
+ }
17681
+ case "text_contains": {
17682
+ const text = await page.locator(assertion.selector).textContent() ?? "";
17683
+ const expected = String(assertion.expected ?? "");
17684
+ return {
17685
+ assertion,
17686
+ passed: text.includes(expected),
17687
+ actual: text
17688
+ };
17689
+ }
17690
+ case "text_equals": {
17691
+ const text = await page.locator(assertion.selector).textContent() ?? "";
17692
+ const expected = String(assertion.expected ?? "");
17693
+ return {
17694
+ assertion,
17695
+ passed: text.trim() === expected.trim(),
17696
+ actual: text
17697
+ };
17698
+ }
17699
+ case "element_count": {
17700
+ const count = await page.locator(assertion.selector).count();
17701
+ const expected = Number(assertion.expected ?? 0);
17702
+ return {
17703
+ assertion,
17704
+ passed: count === expected,
17705
+ actual: String(count)
17706
+ };
17707
+ }
17708
+ case "no_console_errors": {
17709
+ if (context.consoleErrors !== undefined) {
17710
+ const errors = context.consoleErrors.filter(Boolean);
17711
+ return {
17712
+ assertion,
17713
+ passed: errors.length === 0,
17714
+ actual: errors.length === 0 ? "No console errors captured" : errors.slice(0, 3).join(" | ")
17715
+ };
17716
+ }
17717
+ const errorElements = await page.locator('[role="alert"], .error, .error-message, [data-testid="error"]').count();
17718
+ return {
17719
+ assertion,
17720
+ passed: errorElements === 0,
17721
+ actual: `${errorElements} error element(s) found`
17722
+ };
17723
+ }
17724
+ case "no_a11y_violations": {
17725
+ try {
17726
+ const auditResult = await runA11yAudit(page);
17727
+ const hasIssues = auditResult.violations.length > 0;
17728
+ return {
17729
+ assertion,
17730
+ passed: !hasIssues,
17731
+ actual: hasIssues ? `${auditResult.totalViolations} violation(s): ${auditResult.violations.map((v) => v.id).join(", ")}` : "No accessibility violations found"
17732
+ };
17733
+ } catch (err) {
17734
+ return {
17735
+ assertion,
17736
+ passed: false,
17737
+ actual: "",
17738
+ error: err instanceof Error ? err.message : String(err)
17739
+ };
17740
+ }
17741
+ }
17742
+ case "url_contains": {
17743
+ const url = page.url();
17744
+ const expected = String(assertion.expected ?? "");
17745
+ return {
17746
+ assertion,
17747
+ passed: url.includes(expected),
17748
+ actual: url
17749
+ };
17750
+ }
17751
+ case "title_contains": {
17752
+ const title = await page.title();
17753
+ const expected = String(assertion.expected ?? "");
17754
+ return {
17755
+ assertion,
17756
+ passed: title.includes(expected),
17757
+ actual: title
17758
+ };
17759
+ }
17760
+ case "cookie_exists": {
17761
+ const cookieName = assertion.expected;
17762
+ const cookies = await page.context().cookies();
17763
+ const found = cookies.some((c) => c.name === cookieName);
17764
+ return {
17765
+ assertion,
17766
+ passed: found,
17767
+ actual: found ? `Cookie "${cookieName}" exists` : `Cookie "${cookieName}" not found`
17768
+ };
17769
+ }
17770
+ case "cookie_not_exists": {
17771
+ const cookieName = assertion.expected;
17772
+ const cookies = await page.context().cookies();
17773
+ const found = cookies.some((c) => c.name === cookieName);
17774
+ return {
17775
+ assertion,
17776
+ passed: !found,
17777
+ actual: found ? `Cookie "${cookieName}" found (unexpected)` : `Cookie "${cookieName}" does not exist`
17778
+ };
17779
+ }
17780
+ case "cookie_value": {
17781
+ const [cookieName, expectedValue] = assertion.expected.split("=", 2);
17782
+ const cookies = await page.context().cookies();
17783
+ const cookie = cookies.find((c) => c.name === cookieName);
17784
+ const actualValue = cookie?.value ?? "";
17785
+ return {
17786
+ assertion,
17787
+ passed: actualValue === expectedValue,
17788
+ actual: cookie ? `${cookieName}=${actualValue}` : `Cookie "${cookieName}" not found`
17789
+ };
17790
+ }
17791
+ case "local_storage_exists": {
17792
+ const key = assertion.expected;
17793
+ const value = await page.evaluate((k) => localStorage.getItem(k), key);
17794
+ return {
17795
+ assertion,
17796
+ passed: value !== null,
17797
+ actual: value !== null ? `Key "${key}" exists with value "${value}"` : `Key "${key}" not found in localStorage`
17798
+ };
17799
+ }
17800
+ case "local_storage_not_exists": {
17801
+ const key = assertion.expected;
17802
+ const value = await page.evaluate((k) => localStorage.getItem(k), key);
17803
+ return {
17804
+ assertion,
17805
+ passed: value === null,
17806
+ actual: value !== null ? `Key "${key}" exists (unexpected)` : `Key "${key}" does not exist in localStorage`
17807
+ };
17808
+ }
17809
+ case "local_storage_value": {
17810
+ const [lsKey, expectedValue] = assertion.expected.split("=", 2);
17811
+ const value = await page.evaluate((k) => localStorage.getItem(k), lsKey ?? "");
17812
+ return {
17813
+ assertion,
17814
+ passed: value === expectedValue,
17815
+ actual: value !== null ? `${lsKey}=${value}` : `Key "${lsKey}" not found in localStorage`
17816
+ };
17817
+ }
17818
+ case "session_storage_value": {
17819
+ const [ssKey, expectedValue] = assertion.expected.split("=", 2);
17820
+ const value = await page.evaluate((k) => sessionStorage.getItem(k), ssKey ?? "");
17821
+ return {
17822
+ assertion,
17823
+ passed: value === expectedValue,
17824
+ actual: value !== null ? `${ssKey}=${value}` : `Key "${ssKey}" not found in sessionStorage`
17825
+ };
17826
+ }
17827
+ case "session_storage_not_exists": {
17828
+ const key = assertion.expected;
17829
+ const value = await page.evaluate((k) => sessionStorage.getItem(k), key);
17830
+ return {
17831
+ assertion,
17832
+ passed: value === null,
17833
+ actual: value !== null ? `Key "${key}" exists (unexpected)` : `Key "${key}" does not exist in sessionStorage`
17834
+ };
17835
+ }
17836
+ default: {
17837
+ return {
17838
+ assertion,
17839
+ passed: false,
17840
+ actual: "",
17841
+ error: `Unknown assertion type: ${assertion.type}`
17842
+ };
17843
+ }
17844
+ }
17845
+ }
17846
+ function parseAssertionString(str) {
17847
+ const trimmed = str.trim();
17848
+ if (trimmed === "no-console-errors") {
17849
+ return { type: "no_console_errors", description: "No console errors" };
17850
+ }
17851
+ if (trimmed.startsWith("url:contains:")) {
17852
+ const expected = trimmed.slice("url:contains:".length);
17853
+ return { type: "url_contains", expected, description: `URL contains "${expected}"` };
17854
+ }
17855
+ if (trimmed.startsWith("title:contains:")) {
17856
+ const expected = trimmed.slice("title:contains:".length);
17857
+ return { type: "title_contains", expected, description: `Title contains "${expected}"` };
17858
+ }
17859
+ if (trimmed.startsWith("count:")) {
17860
+ const rest = trimmed.slice("count:".length);
17861
+ const eqIdx = rest.indexOf(" eq:");
17862
+ if (eqIdx === -1) {
17863
+ throw new Error(`Invalid count assertion format: ${str}. Expected "count:<selector> eq:<number>"`);
17864
+ }
17865
+ const selector = rest.slice(0, eqIdx);
17866
+ const expected = parseInt(rest.slice(eqIdx + " eq:".length), 10);
17867
+ return { type: "element_count", selector, expected, description: `${selector} count equals ${expected}` };
17868
+ }
17869
+ if (trimmed.startsWith("text:")) {
17870
+ const rest = trimmed.slice("text:".length);
17871
+ const containsIdx = rest.indexOf(" contains:");
17872
+ const equalsIdx = rest.indexOf(" equals:");
17873
+ if (containsIdx !== -1) {
17874
+ const selector = rest.slice(0, containsIdx);
17875
+ const expected = rest.slice(containsIdx + " contains:".length);
17876
+ return { type: "text_contains", selector, expected, description: `${selector} text contains "${expected}"` };
17877
+ }
17878
+ if (equalsIdx !== -1) {
17879
+ const selector = rest.slice(0, equalsIdx);
17880
+ const expected = rest.slice(equalsIdx + " equals:".length);
17881
+ return { type: "text_equals", selector, expected, description: `${selector} text equals "${expected}"` };
17882
+ }
17883
+ throw new Error(`Invalid text assertion format: ${str}. Expected "text:<selector> contains:<text>" or "text:<selector> equals:<text>"`);
17884
+ }
17885
+ if (trimmed.startsWith("selector:")) {
17886
+ const rest = trimmed.slice("selector:".length);
17887
+ const lastSpace = rest.lastIndexOf(" ");
17888
+ if (lastSpace === -1) {
17889
+ throw new Error(`Invalid selector assertion format: ${str}. Expected "selector:<selector> visible" or "selector:<selector> not-visible"`);
17890
+ }
17891
+ const selector = rest.slice(0, lastSpace);
17892
+ const action = rest.slice(lastSpace + 1);
17893
+ if (action === "visible") {
17894
+ return { type: "visible", selector, description: `${selector} is visible` };
17895
+ }
17896
+ if (action === "not-visible") {
17897
+ return { type: "not_visible", selector, description: `${selector} is not visible` };
17898
+ }
17899
+ throw new Error(`Unknown selector action: "${action}". Expected "visible" or "not-visible"`);
17900
+ }
17901
+ if (trimmed.startsWith("cookie:exists:")) {
17902
+ const name = trimmed.slice("cookie:exists:".length);
17903
+ return { type: "cookie_exists", expected: name, description: `Cookie "${name}" exists` };
17904
+ }
17905
+ if (trimmed.startsWith("cookie:not-exists:")) {
17906
+ const name = trimmed.slice("cookie:not-exists:".length);
17907
+ return { type: "cookie_not_exists", expected: name, description: `Cookie "${name}" does not exist` };
17908
+ }
17909
+ if (trimmed.startsWith("cookie:value:")) {
17910
+ const valueStr = trimmed.slice("cookie:value:".length);
17911
+ return { type: "cookie_value", expected: valueStr, description: `Cookie value is "${valueStr}"` };
17912
+ }
17913
+ if (trimmed.startsWith("local:exists:")) {
17914
+ const key = trimmed.slice("local:exists:".length);
17915
+ return { type: "local_storage_exists", expected: key, description: `LocalStorage key "${key}" exists` };
17916
+ }
17917
+ if (trimmed.startsWith("local:not-exists:")) {
17918
+ const key = trimmed.slice("local:not-exists:".length);
17919
+ return { type: "local_storage_not_exists", expected: key, description: `LocalStorage key "${key}" does not exist` };
17920
+ }
17921
+ if (trimmed.startsWith("local:value:")) {
17922
+ const valueStr = trimmed.slice("local:value:".length);
17923
+ return { type: "local_storage_value", expected: valueStr, description: `LocalStorage value is "${valueStr}"` };
17924
+ }
17925
+ if (trimmed.startsWith("session:value:")) {
17926
+ const valueStr = trimmed.slice("session:value:".length);
17927
+ return { type: "session_storage_value", expected: valueStr, description: `SessionStorage value is "${valueStr}"` };
17928
+ }
17929
+ if (trimmed.startsWith("session:not-exists:")) {
17930
+ const key = trimmed.slice("session:not-exists:".length);
17931
+ return { type: "session_storage_not_exists", expected: key, description: `SessionStorage key "${key}" does not exist` };
17932
+ }
17933
+ throw new Error(`Cannot parse assertion: "${str}". See --help for assertion formats.`);
17934
+ }
17935
+ function allAssertionsPassed(results) {
17936
+ return results.every((r) => r.passed);
17937
+ }
17938
+ function formatAssertionResults(results) {
17939
+ if (results.length === 0)
17940
+ return "No assertions.";
17941
+ const lines = [];
17942
+ for (const r of results) {
17943
+ const icon = r.passed ? "PASS" : "FAIL";
17944
+ const desc = r.assertion.description || `${r.assertion.type}${r.assertion.selector ? ` ${r.assertion.selector}` : ""}`;
17945
+ let line = ` [${icon}] ${desc}`;
17946
+ if (!r.passed) {
17947
+ line += ` (actual: ${r.actual})`;
17948
+ if (r.error)
17949
+ line += ` \u2014 ${r.error}`;
17950
+ }
17951
+ lines.push(line);
17952
+ }
17953
+ const passed = results.filter((r) => r.passed).length;
17954
+ lines.push(`
17955
+ ${passed}/${results.length} assertions passed.`);
17956
+ return lines.join(`
17957
+ `);
17958
+ }
17959
+ var init_assertions = () => {};
17960
+
17497
17961
  // src/db/flows.ts
17498
17962
  var exports_flows = {};
17499
17963
  __export(exports_flows, {
@@ -17652,7 +18116,9 @@ __export(exports_runner, {
17652
18116
  runSingleScenario: () => runSingleScenario,
17653
18117
  runByFilter: () => runByFilter,
17654
18118
  runBatch: () => runBatch,
17655
- onRunEvent: () => onRunEvent
18119
+ resolveScenariosForRun: () => resolveScenariosForRun,
18120
+ onRunEvent: () => onRunEvent,
18121
+ applyStructuredAssertionsToResult: () => applyStructuredAssertionsToResult
17656
18122
  });
17657
18123
  import { mkdirSync as mkdirSync8 } from "fs";
17658
18124
  import { join as join13 } from "path";
@@ -17664,6 +18130,54 @@ function emit(event) {
17664
18130
  if (eventHandler)
17665
18131
  eventHandler(event);
17666
18132
  }
18133
+ function assertionDescription(result) {
18134
+ return result.assertion.description || `${result.assertion.type}${result.assertion.selector ? ` ${result.assertion.selector}` : ""}`;
18135
+ }
18136
+ function summarizeAssertionResult(result) {
18137
+ const description = assertionDescription(result);
18138
+ if (result.passed)
18139
+ return description;
18140
+ const suffix = result.error ? `; ${result.error}` : "";
18141
+ return `${description} (actual: ${result.actual}${suffix})`;
18142
+ }
18143
+ async function applyStructuredAssertionsToResult(input) {
18144
+ const assertions = input.scenario.assertions ?? [];
18145
+ if (assertions.length === 0) {
18146
+ return {
18147
+ status: input.status,
18148
+ reasoning: input.reasoning,
18149
+ assertionsPassed: [],
18150
+ assertionsFailed: [],
18151
+ assertionResults: []
18152
+ };
18153
+ }
18154
+ const results = await evaluateAssertions(input.page, assertions, {
18155
+ consoleErrors: input.consoleErrors
18156
+ });
18157
+ const assertionsPassed = results.filter((r) => r.passed).map(summarizeAssertionResult);
18158
+ const assertionsFailed = results.filter((r) => !r.passed).map(summarizeAssertionResult);
18159
+ const assertionResults = results.map((result) => ({
18160
+ type: result.assertion.type,
18161
+ description: assertionDescription(result),
18162
+ passed: result.passed,
18163
+ actual: result.actual,
18164
+ ...result.error ? { error: result.error } : {}
18165
+ }));
18166
+ const assertionsOk = allAssertionsPassed(results);
18167
+ const status = assertionsOk || input.status !== "passed" ? input.status : "failed";
18168
+ const assertionHeading = assertionsOk ? "Structured assertions passed:" : "Structured assertions failed:";
18169
+ const reasoningParts = [input.reasoning, `${assertionHeading}
18170
+ ${formatAssertionResults(results)}`].map((part) => part.trim()).filter(Boolean);
18171
+ return {
18172
+ status,
18173
+ reasoning: reasoningParts.join(`
18174
+
18175
+ `),
18176
+ assertionsPassed,
18177
+ assertionsFailed,
18178
+ assertionResults
18179
+ };
18180
+ }
17667
18181
  function withTimeout(promise, ms, label) {
17668
18182
  return new Promise((resolve, reject) => {
17669
18183
  const warningAt = Math.floor(ms * 0.8);
@@ -17834,6 +18348,7 @@ async function runSingleScenario(scenario, runId, options) {
17834
18348
  model,
17835
18349
  runId,
17836
18350
  sessionId: result.id,
18351
+ baseUrl: options.url,
17837
18352
  maxTurns: effectiveOptions.minimal ? 10 : 30,
17838
18353
  a11y: effectiveOptions.a11y,
17839
18354
  persona: persona ? {
@@ -17916,27 +18431,46 @@ async function runSingleScenario(scenario, runId, options) {
17916
18431
  closeSession(result.id);
17917
18432
  const lightpandaNote = options.engine === "lightpanda" ? " (Running with Lightpanda \u2014 no screenshots)" : options.engine === "bun" ? " (Running with Bun.WebView \u2014 native, ~11x faster)" : "";
17918
18433
  const networkMeta = networkErrors.length > 0 ? { networkErrors: networkErrors.slice(0, 20) } : {};
17919
- let updatedResult = updateResult(result.id, {
18434
+ const baseReasoning = agentResult.reasoning ? agentResult.reasoning + lightpandaNote : lightpandaNote || "";
18435
+ const assertionOutcome = await applyStructuredAssertionsToResult({
18436
+ page,
18437
+ scenario,
18438
+ consoleErrors,
17920
18439
  status: agentResult.status,
17921
- reasoning: agentResult.reasoning ? agentResult.reasoning + lightpandaNote : lightpandaNote || undefined,
18440
+ reasoning: baseReasoning
18441
+ });
18442
+ const structuredAssertionMeta = assertionOutcome.assertionResults.length > 0 ? {
18443
+ structuredAssertions: {
18444
+ passed: assertionOutcome.assertionsPassed,
18445
+ failed: assertionOutcome.assertionsFailed,
18446
+ results: assertionOutcome.assertionResults
18447
+ }
18448
+ } : {};
18449
+ let updatedResult = updateResult(result.id, {
18450
+ status: assertionOutcome.status,
18451
+ reasoning: assertionOutcome.reasoning || undefined,
17922
18452
  stepsCompleted: agentResult.stepsCompleted,
17923
18453
  durationMs: Date.now() - new Date(result.createdAt).getTime(),
17924
18454
  tokensUsed: agentResult.tokensUsed,
17925
18455
  costCents: estimateCost(model, agentResult.tokensUsed),
17926
- metadata: { consoleLogs, ...networkErrors.length > 0 ? networkMeta : {} }
18456
+ metadata: {
18457
+ consoleLogs,
18458
+ ...networkErrors.length > 0 ? networkMeta : {},
18459
+ ...structuredAssertionMeta
18460
+ }
17927
18461
  });
17928
- if (agentResult.status === "failed" || agentResult.status === "error") {
17929
- const failureAnalysis = analyzeFailure(null, agentResult.reasoning ?? null);
18462
+ if (assertionOutcome.status === "failed" || assertionOutcome.status === "error") {
18463
+ const failureAnalysis = analyzeFailure(null, assertionOutcome.reasoning ?? null);
17930
18464
  if (failureAnalysis) {
17931
18465
  updatedResult = updateResult(result.id, { failureAnalysis });
17932
18466
  }
17933
18467
  }
17934
- if (agentResult.status === "passed") {
18468
+ if (assertionOutcome.status === "passed") {
17935
18469
  try {
17936
18470
  updateScenarioPassedCache(scenario.id, options.url);
17937
18471
  } catch {}
17938
18472
  }
17939
- const eventType = agentResult.status === "passed" ? "scenario:pass" : "scenario:fail";
18473
+ const eventType = assertionOutcome.status === "passed" ? "scenario:pass" : "scenario:fail";
17940
18474
  emit({ type: eventType, scenarioId: scenario.id, scenarioName: scenario.name, resultId: result.id, runId });
17941
18475
  return updatedResult;
17942
18476
  } catch (error) {
@@ -17961,7 +18495,8 @@ async function runSingleScenario(scenario, runId, options) {
17961
18495
  } finally {
17962
18496
  if (harPath) {
17963
18497
  try {
17964
- updateResult(result.id, { metadata: { harPath } });
18498
+ const existing = getResult(result.id);
18499
+ updateResult(result.id, { metadata: { ...existing?.metadata ?? {}, harPath } });
17965
18500
  } catch {}
17966
18501
  }
17967
18502
  if (browser) {
@@ -18133,22 +18668,31 @@ async function runBatch(scenarios, options) {
18133
18668
  }
18134
18669
  return { run: finalRun, results };
18135
18670
  }
18136
- async function runByFilter(options) {
18137
- let scenarios;
18671
+ function findScenarioInList(scenarios, id) {
18672
+ return scenarios.find((scenario) => scenario.id === id || scenario.shortId === id || scenario.id.startsWith(id)) ?? null;
18673
+ }
18674
+ function resolveScenariosForRun(options) {
18138
18675
  if (options.scenarioIds && options.scenarioIds.length > 0) {
18139
- const all = listScenarios({ projectId: options.projectId });
18140
- scenarios = all.filter((s) => options.scenarioIds.includes(s.id) || options.scenarioIds.includes(s.shortId));
18141
- if (scenarios.length === 0 && options.projectId) {
18142
- const global2 = listScenarios({});
18143
- scenarios = global2.filter((s) => options.scenarioIds.includes(s.id) || options.scenarioIds.includes(s.shortId));
18676
+ const scoped = listScenarios({ projectId: options.projectId });
18677
+ const resolved = [];
18678
+ const seen = new Set;
18679
+ for (const id of options.scenarioIds) {
18680
+ const scenario = findScenarioInList(scoped, id) ?? getScenario(id);
18681
+ if (scenario && !seen.has(scenario.id)) {
18682
+ resolved.push(scenario);
18683
+ seen.add(scenario.id);
18684
+ }
18144
18685
  }
18145
- } else {
18146
- scenarios = listScenarios({
18147
- projectId: options.projectId,
18148
- tags: options.tags,
18149
- priority: options.priority
18150
- });
18686
+ return resolved;
18151
18687
  }
18688
+ return listScenarios({
18689
+ projectId: options.projectId,
18690
+ tags: options.tags,
18691
+ priority: options.priority
18692
+ });
18693
+ }
18694
+ async function runByFilter(options) {
18695
+ const scenarios = resolveScenariosForRun(options);
18152
18696
  if (scenarios.length === 0) {
18153
18697
  const config = loadConfig();
18154
18698
  const model = resolveModel(options.model ?? config.defaultModel);
@@ -18161,17 +18705,7 @@ async function runByFilter(options) {
18161
18705
  function startRunAsync(options) {
18162
18706
  const config = loadConfig();
18163
18707
  const model = resolveModel(options.model ?? config.defaultModel);
18164
- let scenarios;
18165
- if (options.scenarioIds && options.scenarioIds.length > 0) {
18166
- const all = listScenarios({ projectId: options.projectId });
18167
- scenarios = all.filter((s) => options.scenarioIds.includes(s.id) || options.scenarioIds.includes(s.shortId));
18168
- } else {
18169
- scenarios = listScenarios({
18170
- projectId: options.projectId,
18171
- tags: options.tags,
18172
- priority: options.priority
18173
- });
18174
- }
18708
+ const scenarios = resolveScenariosForRun(options);
18175
18709
  if (!options.skipBudgetCheck) {
18176
18710
  const cap = options.maxCostCents ?? config.defaultMaxCostCents;
18177
18711
  if (cap !== undefined && cap > 0 && scenarios.length > 0) {
@@ -18275,6 +18809,7 @@ var init_runner = __esm(() => {
18275
18809
  init_session_tracker();
18276
18810
  init_webhooks();
18277
18811
  init_failure_pipeline();
18812
+ init_assertions();
18278
18813
  });
18279
18814
 
18280
18815
  // src/lib/reporter.ts
@@ -25414,18 +25949,7 @@ function normalizeFilter(input) {
25414
25949
  };
25415
25950
  }
25416
25951
  function normalizeExecution(input) {
25417
- const target = input?.target ?? "local";
25418
- if (target === "connector:e2b") {
25419
- return {
25420
- target,
25421
- connector: input?.connector ?? "e2b",
25422
- operation: input?.operation ?? "run",
25423
- sandboxTemplate: input?.sandboxTemplate,
25424
- timeoutMs: input?.timeoutMs,
25425
- env: input?.env
25426
- };
25427
- }
25428
- return { ...DEFAULT_EXECUTION, timeoutMs: input?.timeoutMs };
25952
+ return input ? workflowExecutionFromValue(input) : DEFAULT_EXECUTION;
25429
25953
  }
25430
25954
  function createTestingWorkflow(input) {
25431
25955
  const db2 = getDatabase();
@@ -26029,11 +26553,11 @@ import { existsSync as existsSync42, writeFileSync as writeFileSync32, readFileS
26029
26553
  import { join as join42 } from "path";
26030
26554
  import { Database as Database4 } from "bun:sqlite";
26031
26555
  import { existsSync as existsSync16, mkdirSync as mkdirSync13 } from "fs";
26032
- import { dirname as dirname4, join as join18, resolve as resolve3 } from "path";
26033
- import { existsSync as existsSync22, writeFileSync as writeFileSync6 } from "fs";
26556
+ import { dirname as dirname4, join as join19, resolve as resolve2 } from "path";
26557
+ import { existsSync as existsSync22, writeFileSync as writeFileSync7 } from "fs";
26034
26558
  import { join as join22 } from "path";
26035
26559
  import { execSync as execSync3, execFileSync } from "child_process";
26036
- import { existsSync as existsSync32, readFileSync as readFileSync8, writeFileSync as writeFileSync22, mkdirSync as mkdirSync22 } from "fs";
26560
+ import { existsSync as existsSync32, readFileSync as readFileSync7, writeFileSync as writeFileSync22, mkdirSync as mkdirSync22 } from "fs";
26037
26561
  import { homedir as homedir11 } from "os";
26038
26562
  import { join as join32, dirname as dirname22 } from "path";
26039
26563
  import { hostname } from "os";
@@ -26204,12 +26728,12 @@ function getDbPath2() {
26204
26728
  return process.env["PROJECTS_DB_PATH"];
26205
26729
  }
26206
26730
  const home = process.env["HOME"] || process.env["USERPROFILE"] || "~";
26207
- return join18(home, ".hasna", "projects", "projects.db");
26731
+ return join19(home, ".hasna", "projects", "projects.db");
26208
26732
  }
26209
26733
  function ensureDir2(filePath) {
26210
26734
  if (filePath === ":memory:")
26211
26735
  return;
26212
- const dir = dirname4(resolve3(filePath));
26736
+ const dir = dirname4(resolve2(filePath));
26213
26737
  if (!existsSync16(dir)) {
26214
26738
  mkdirSync13(dir, { recursive: true });
26215
26739
  }
@@ -26249,7 +26773,7 @@ function gitInit(project) {
26249
26773
  execSync3("git init", { cwd: path, stdio: "pipe" });
26250
26774
  const gitignorePath = join22(path, ".gitignore");
26251
26775
  if (!existsSync22(gitignorePath)) {
26252
- writeFileSync6(gitignorePath, GITIGNORE_TEMPLATE, "utf-8");
26776
+ writeFileSync7(gitignorePath, GITIGNORE_TEMPLATE, "utf-8");
26253
26777
  }
26254
26778
  const projectJson = {
26255
26779
  id,
@@ -26258,7 +26782,7 @@ function gitInit(project) {
26258
26782
  created_at: project.created_at,
26259
26783
  integrations: project.integrations ?? {}
26260
26784
  };
26261
- writeFileSync6(join22(path, ".project.json"), JSON.stringify(projectJson, null, 2) + `
26785
+ writeFileSync7(join22(path, ".project.json"), JSON.stringify(projectJson, null, 2) + `
26262
26786
  `, "utf-8");
26263
26787
  execSync3("git add .gitignore .project.json", { cwd: path, stdio: "pipe" });
26264
26788
  execSync3(`git commit -m "chore: init project ${name}"`, {
@@ -26270,7 +26794,7 @@ function gitInit(project) {
26270
26794
  function getConfig() {
26271
26795
  if (existsSync32(CONFIG_PATH3)) {
26272
26796
  try {
26273
- const user = JSON.parse(readFileSync8(CONFIG_PATH3, "utf-8"));
26797
+ const user = JSON.parse(readFileSync7(CONFIG_PATH3, "utf-8"));
26274
26798
  return { ...DEFAULTS, ...user };
26275
26799
  } catch {
26276
26800
  return { ...DEFAULTS };
@@ -42345,11 +42869,11 @@ More information can be found at: https://a.co/c895JFp`);
42345
42869
  var numberSelector = (obj, key, type2) => {
42346
42870
  if (!(key in obj))
42347
42871
  return;
42348
- const numberValue = parseInt(obj[key], 10);
42349
- if (Number.isNaN(numberValue)) {
42872
+ const numberValue2 = parseInt(obj[key], 10);
42873
+ if (Number.isNaN(numberValue2)) {
42350
42874
  throw new TypeError(`Cannot load ${type2} '${key}'. Expected number, got '${obj[key]}'.`);
42351
42875
  }
42352
- return numberValue;
42876
+ return numberValue2;
42353
42877
  };
42354
42878
  exports.SelectorType = undefined;
42355
42879
  (function(SelectorType2) {
@@ -57095,7 +57619,7 @@ __export(exports_openapi_import, {
57095
57619
  importFromOpenAPI: () => importFromOpenAPI,
57096
57620
  importApiChecksFromOpenAPI: () => importApiChecksFromOpenAPI
57097
57621
  });
57098
- import { readFileSync as readFileSync9 } from "fs";
57622
+ import { readFileSync as readFileSync8 } from "fs";
57099
57623
  function parseSpec(content) {
57100
57624
  try {
57101
57625
  return JSON.parse(content);
@@ -57124,7 +57648,7 @@ function parseOpenAPISpec(filePathOrUrl) {
57124
57648
  if (filePathOrUrl.startsWith("http")) {
57125
57649
  throw new Error("URL fetching not supported yet. Download the spec file first.");
57126
57650
  }
57127
- content = readFileSync9(filePathOrUrl, "utf-8");
57651
+ content = readFileSync8(filePathOrUrl, "utf-8");
57128
57652
  const spec = parseSpec(content);
57129
57653
  const isOpenAPI3 = !!spec.openapi;
57130
57654
  const isSwagger2 = !!spec.swagger;
@@ -57187,7 +57711,7 @@ function parseOpenAPISpecAsChecks(filePathOrUrl) {
57187
57711
  if (filePathOrUrl.startsWith("http")) {
57188
57712
  throw new Error("URL fetching not supported yet. Download the spec file first.");
57189
57713
  }
57190
- content = readFileSync9(filePathOrUrl, "utf-8");
57714
+ content = readFileSync8(filePathOrUrl, "utf-8");
57191
57715
  const spec = parseSpec(content);
57192
57716
  if (!spec.openapi && !spec.swagger) {
57193
57717
  throw new Error("Not a valid OpenAPI 3.x or Swagger 2.0 spec");
@@ -57527,7 +58051,7 @@ async function recordSession(url, options) {
57527
58051
  await Promise.race([
57528
58052
  page.waitForEvent("close").catch(() => {}),
57529
58053
  context.waitForEvent("close").catch(() => {}),
57530
- new Promise((resolve4) => setTimeout(resolve4, timeout))
58054
+ new Promise((resolve3) => setTimeout(resolve3, timeout))
57531
58055
  ]);
57532
58056
  clearInterval(pollInterval);
57533
58057
  try {
@@ -75898,7 +76422,7 @@ function createProviderToolFactoryWithOutputSchema({
75898
76422
  supportsDeferredResults
75899
76423
  });
75900
76424
  }
75901
- async function resolve4(value) {
76425
+ async function resolve3(value) {
75902
76426
  if (typeof value === "function") {
75903
76427
  value = value();
75904
76428
  }
@@ -77585,7 +78109,7 @@ var import_oidc, import_oidc2, marker17 = "vercel.ai.gateway.error", symbol18, _
77585
78109
  try {
77586
78110
  const { value } = await getFromApi({
77587
78111
  url: `${this.config.baseURL}/config`,
77588
- headers: await resolve4(this.config.headers()),
78112
+ headers: await resolve3(this.config.headers()),
77589
78113
  successfulResponseHandler: createJsonResponseHandler(gatewayAvailableModelsResponseSchema),
77590
78114
  failedResponseHandler: createJsonErrorResponseHandler({
77591
78115
  errorSchema: exports_external2.any(),
@@ -77603,7 +78127,7 @@ var import_oidc, import_oidc2, marker17 = "vercel.ai.gateway.error", symbol18, _
77603
78127
  const baseUrl = new URL(this.config.baseURL);
77604
78128
  const { value } = await getFromApi({
77605
78129
  url: `${baseUrl.origin}/v1/credits`,
77606
- headers: await resolve4(this.config.headers()),
78130
+ headers: await resolve3(this.config.headers()),
77607
78131
  successfulResponseHandler: createJsonResponseHandler(gatewayCreditsResponseSchema),
77608
78132
  failedResponseHandler: createJsonErrorResponseHandler({
77609
78133
  errorSchema: exports_external2.any(),
@@ -77649,7 +78173,7 @@ var import_oidc, import_oidc2, marker17 = "vercel.ai.gateway.error", symbol18, _
77649
78173
  }
77650
78174
  const { value } = await getFromApi({
77651
78175
  url: `${baseUrl.origin}/v1/report?${searchParams.toString()}`,
77652
- headers: await resolve4(this.config.headers()),
78176
+ headers: await resolve3(this.config.headers()),
77653
78177
  successfulResponseHandler: createJsonResponseHandler(gatewaySpendReportResponseSchema),
77654
78178
  failedResponseHandler: createJsonErrorResponseHandler({
77655
78179
  errorSchema: exports_external2.any(),
@@ -77671,7 +78195,7 @@ var import_oidc, import_oidc2, marker17 = "vercel.ai.gateway.error", symbol18, _
77671
78195
  const baseUrl = new URL(this.config.baseURL);
77672
78196
  const { value } = await getFromApi({
77673
78197
  url: `${baseUrl.origin}/v1/generation?id=${encodeURIComponent(params.id)}`,
77674
- headers: await resolve4(this.config.headers()),
78198
+ headers: await resolve3(this.config.headers()),
77675
78199
  successfulResponseHandler: createJsonResponseHandler(gatewayGenerationInfoResponseSchema),
77676
78200
  failedResponseHandler: createJsonErrorResponseHandler({
77677
78201
  errorSchema: exports_external2.any(),
@@ -77704,7 +78228,7 @@ var import_oidc, import_oidc2, marker17 = "vercel.ai.gateway.error", symbol18, _
77704
78228
  async doGenerate(options) {
77705
78229
  const { args, warnings } = await this.getArgs(options);
77706
78230
  const { abortSignal } = options;
77707
- const resolvedHeaders = await resolve4(this.config.headers());
78231
+ const resolvedHeaders = await resolve3(this.config.headers());
77708
78232
  try {
77709
78233
  const {
77710
78234
  responseHeaders,
@@ -77712,7 +78236,7 @@ var import_oidc, import_oidc2, marker17 = "vercel.ai.gateway.error", symbol18, _
77712
78236
  rawValue: rawResponse
77713
78237
  } = await postJsonToApi({
77714
78238
  url: this.getUrl(),
77715
- headers: combineHeaders(resolvedHeaders, options.headers, this.getModelConfigHeaders(this.modelId, false), await resolve4(this.config.o11yHeaders)),
78239
+ headers: combineHeaders(resolvedHeaders, options.headers, this.getModelConfigHeaders(this.modelId, false), await resolve3(this.config.o11yHeaders)),
77716
78240
  body: args,
77717
78241
  successfulResponseHandler: createJsonResponseHandler(exports_external2.any()),
77718
78242
  failedResponseHandler: createJsonErrorResponseHandler({
@@ -77735,11 +78259,11 @@ var import_oidc, import_oidc2, marker17 = "vercel.ai.gateway.error", symbol18, _
77735
78259
  async doStream(options) {
77736
78260
  const { args, warnings } = await this.getArgs(options);
77737
78261
  const { abortSignal } = options;
77738
- const resolvedHeaders = await resolve4(this.config.headers());
78262
+ const resolvedHeaders = await resolve3(this.config.headers());
77739
78263
  try {
77740
78264
  const { value: response, responseHeaders } = await postJsonToApi({
77741
78265
  url: this.getUrl(),
77742
- headers: combineHeaders(resolvedHeaders, options.headers, this.getModelConfigHeaders(this.modelId, true), await resolve4(this.config.o11yHeaders)),
78266
+ headers: combineHeaders(resolvedHeaders, options.headers, this.getModelConfigHeaders(this.modelId, true), await resolve3(this.config.o11yHeaders)),
77743
78267
  body: args,
77744
78268
  successfulResponseHandler: createEventSourceResponseHandler(exports_external2.any()),
77745
78269
  failedResponseHandler: createJsonErrorResponseHandler({
@@ -77824,7 +78348,7 @@ var import_oidc, import_oidc2, marker17 = "vercel.ai.gateway.error", symbol18, _
77824
78348
  providerOptions
77825
78349
  }) {
77826
78350
  var _a92;
77827
- const resolvedHeaders = await resolve4(this.config.headers());
78351
+ const resolvedHeaders = await resolve3(this.config.headers());
77828
78352
  try {
77829
78353
  const {
77830
78354
  responseHeaders,
@@ -77832,7 +78356,7 @@ var import_oidc, import_oidc2, marker17 = "vercel.ai.gateway.error", symbol18, _
77832
78356
  rawValue
77833
78357
  } = await postJsonToApi({
77834
78358
  url: this.getUrl(),
77835
- headers: combineHeaders(resolvedHeaders, headers != null ? headers : {}, this.getModelConfigHeaders(), await resolve4(this.config.o11yHeaders)),
78359
+ headers: combineHeaders(resolvedHeaders, headers != null ? headers : {}, this.getModelConfigHeaders(), await resolve3(this.config.o11yHeaders)),
77836
78360
  body: {
77837
78361
  values,
77838
78362
  ...providerOptions ? { providerOptions } : {}
@@ -77888,7 +78412,7 @@ var import_oidc, import_oidc2, marker17 = "vercel.ai.gateway.error", symbol18, _
77888
78412
  abortSignal
77889
78413
  }) {
77890
78414
  var _a92, _b92, _c2, _d2;
77891
- const resolvedHeaders = await resolve4(this.config.headers());
78415
+ const resolvedHeaders = await resolve3(this.config.headers());
77892
78416
  try {
77893
78417
  const {
77894
78418
  responseHeaders,
@@ -77896,7 +78420,7 @@ var import_oidc, import_oidc2, marker17 = "vercel.ai.gateway.error", symbol18, _
77896
78420
  rawValue
77897
78421
  } = await postJsonToApi({
77898
78422
  url: this.getUrl(),
77899
- headers: combineHeaders(resolvedHeaders, headers != null ? headers : {}, this.getModelConfigHeaders(), await resolve4(this.config.o11yHeaders)),
78423
+ headers: combineHeaders(resolvedHeaders, headers != null ? headers : {}, this.getModelConfigHeaders(), await resolve3(this.config.o11yHeaders)),
77900
78424
  body: {
77901
78425
  prompt,
77902
78426
  n: n2,
@@ -77971,11 +78495,11 @@ var import_oidc, import_oidc2, marker17 = "vercel.ai.gateway.error", symbol18, _
77971
78495
  abortSignal
77972
78496
  }) {
77973
78497
  var _a92;
77974
- const resolvedHeaders = await resolve4(this.config.headers());
78498
+ const resolvedHeaders = await resolve3(this.config.headers());
77975
78499
  try {
77976
78500
  const { responseHeaders, value: responseBody } = await postJsonToApi({
77977
78501
  url: this.getUrl(),
77978
- headers: combineHeaders(resolvedHeaders, headers != null ? headers : {}, this.getModelConfigHeaders(), await resolve4(this.config.o11yHeaders), { accept: "text/event-stream" }),
78502
+ headers: combineHeaders(resolvedHeaders, headers != null ? headers : {}, this.getModelConfigHeaders(), await resolve3(this.config.o11yHeaders), { accept: "text/event-stream" }),
77979
78503
  body: {
77980
78504
  prompt,
77981
78505
  n: n2,
@@ -78098,7 +78622,7 @@ var import_oidc, import_oidc2, marker17 = "vercel.ai.gateway.error", symbol18, _
78098
78622
  abortSignal,
78099
78623
  providerOptions
78100
78624
  }) {
78101
- const resolvedHeaders = await resolve4(this.config.headers());
78625
+ const resolvedHeaders = await resolve3(this.config.headers());
78102
78626
  try {
78103
78627
  const {
78104
78628
  responseHeaders,
@@ -78106,7 +78630,7 @@ var import_oidc, import_oidc2, marker17 = "vercel.ai.gateway.error", symbol18, _
78106
78630
  rawValue
78107
78631
  } = await postJsonToApi({
78108
78632
  url: this.getUrl(),
78109
- headers: combineHeaders(resolvedHeaders, headers != null ? headers : {}, this.getModelConfigHeaders(), await resolve4(this.config.o11yHeaders)),
78633
+ headers: combineHeaders(resolvedHeaders, headers != null ? headers : {}, this.getModelConfigHeaders(), await resolve3(this.config.o11yHeaders)),
78110
78634
  body: {
78111
78635
  documents,
78112
78636
  query,
@@ -87524,7 +88048,7 @@ var import_api2, import_api3, __defProp4, __export4 = (target, all) => {
87524
88048
  const schema = asSchema(inputSchema);
87525
88049
  return {
87526
88050
  name: "object",
87527
- responseFormat: resolve4(schema.jsonSchema).then((jsonSchema2) => ({
88051
+ responseFormat: resolve3(schema.jsonSchema).then((jsonSchema2) => ({
87528
88052
  type: "json",
87529
88053
  schema: jsonSchema2,
87530
88054
  ...name21 != null && { name: name21 },
@@ -87585,7 +88109,7 @@ var import_api2, import_api3, __defProp4, __export4 = (target, all) => {
87585
88109
  const elementSchema = asSchema(inputElementSchema);
87586
88110
  return {
87587
88111
  name: "array",
87588
- responseFormat: resolve4(elementSchema.jsonSchema).then((jsonSchema2) => {
88112
+ responseFormat: resolve3(elementSchema.jsonSchema).then((jsonSchema2) => {
87589
88113
  const { $schema, ...itemSchema } = jsonSchema2;
87590
88114
  return {
87591
88115
  type: "json",
@@ -90482,9 +91006,9 @@ var import_api2, import_api3, __defProp4, __export4 = (target, all) => {
90482
91006
  ...options
90483
91007
  }) {
90484
91008
  var _a21, _b16, _c2, _d2, _e2;
90485
- const resolvedBody = await resolve4(this.body);
90486
- const resolvedHeaders = await resolve4(this.headers);
90487
- const resolvedCredentials = await resolve4(this.credentials);
91009
+ const resolvedBody = await resolve3(this.body);
91010
+ const resolvedHeaders = await resolve3(this.headers);
91011
+ const resolvedCredentials = await resolve3(this.credentials);
90488
91012
  const baseHeaders = {
90489
91013
  ...normalizeHeaders(resolvedHeaders),
90490
91014
  ...normalizeHeaders(options.headers)
@@ -90532,9 +91056,9 @@ var import_api2, import_api3, __defProp4, __export4 = (target, all) => {
90532
91056
  }
90533
91057
  async reconnectToStream(options) {
90534
91058
  var _a21, _b16, _c2, _d2, _e2;
90535
- const resolvedBody = await resolve4(this.body);
90536
- const resolvedHeaders = await resolve4(this.headers);
90537
- const resolvedCredentials = await resolve4(this.credentials);
91059
+ const resolvedBody = await resolve3(this.body);
91060
+ const resolvedHeaders = await resolve3(this.headers);
91061
+ const resolvedCredentials = await resolve3(this.credentials);
90538
91062
  const baseHeaders = {
90539
91063
  ...normalizeHeaders(resolvedHeaders),
90540
91064
  ...normalizeHeaders(options.headers)
@@ -92755,7 +93279,7 @@ __export(exports_session_converter, {
92755
93279
  convertSessionToScenario: () => convertSessionToScenario,
92756
93280
  convertSessionFile: () => convertSessionFile
92757
93281
  });
92758
- import { readFileSync as readFileSync10 } from "fs";
93282
+ import { readFileSync as readFileSync9 } from "fs";
92759
93283
  import { extname } from "path";
92760
93284
  function parseRrwebSession(events) {
92761
93285
  const result = [];
@@ -92941,7 +93465,7 @@ ${condensed}`;
92941
93465
  };
92942
93466
  }
92943
93467
  async function convertSessionFile(filePath, format, options) {
92944
- const raw = readFileSync10(filePath, "utf-8");
93468
+ const raw = readFileSync9(filePath, "utf-8");
92945
93469
  let parsed;
92946
93470
  try {
92947
93471
  parsed = JSON.parse(raw);
@@ -92975,7 +93499,7 @@ function detectSessionFormat(filePath) {
92975
93499
  if (ext === ".har")
92976
93500
  return "har";
92977
93501
  try {
92978
- const content = readFileSync10(filePath, "utf-8").trim();
93502
+ const content = readFileSync9(filePath, "utf-8").trim();
92979
93503
  const parsed = JSON.parse(content);
92980
93504
  if (Array.isArray(parsed) && parsed[0]?.type !== undefined && typeof parsed[0]?.timestamp === "number") {
92981
93505
  return "rrweb";
@@ -93419,7 +93943,7 @@ import chalk6 from "chalk";
93419
93943
  // package.json
93420
93944
  var package_default = {
93421
93945
  name: "@hasna/testers",
93422
- version: "0.0.33",
93946
+ version: "0.0.35",
93423
93947
  description: "AI-powered QA testing CLI \u2014 spawns cheap AI agents to test web apps with headless browsers",
93424
93948
  type: "module",
93425
93949
  main: "dist/index.js",
@@ -93443,10 +93967,10 @@ var package_default = {
93443
93967
  ],
93444
93968
  scripts: {
93445
93969
  build: "bun run build:dashboard && bun run build:cli && bun run build:mcp && bun run build:server && bun run build:lib && bun run build:types",
93446
- "build:cli": "bun build src/cli/index.tsx --outdir dist/cli --target bun --external ink --external react --external chalk --external @modelcontextprotocol/sdk --external @anthropic-ai/sdk --external playwright --external @hasna/browser",
93447
- "build:mcp": "bun build src/mcp/index.ts --outdir dist/mcp --target bun --external @modelcontextprotocol/sdk --external @anthropic-ai/sdk --external playwright --external @hasna/browser",
93448
- "build:server": "bun build src/server/index.ts --outdir dist/server --target bun --external @anthropic-ai/sdk --external playwright --external @hasna/browser",
93449
- "build:lib": "bun build src/index.ts --outdir dist --target bun --external playwright --external @anthropic-ai/sdk --external @modelcontextprotocol/sdk --external @hasna/browser",
93970
+ "build:cli": "bun build src/cli/index.tsx --outdir dist/cli --target bun --external ink --external react --external chalk --external @modelcontextprotocol/sdk --external @anthropic-ai/sdk --external playwright --external @hasna/browser --external @hasna/sandboxes",
93971
+ "build:mcp": "bun build src/mcp/index.ts --outdir dist/mcp --target bun --external @modelcontextprotocol/sdk --external @anthropic-ai/sdk --external playwright --external @hasna/browser --external @hasna/sandboxes",
93972
+ "build:server": "bun build src/server/index.ts --outdir dist/server --target bun --external @anthropic-ai/sdk --external playwright --external @hasna/browser --external @hasna/sandboxes",
93973
+ "build:lib": "bun build src/index.ts --outdir dist --target bun --external playwright --external @anthropic-ai/sdk --external @modelcontextprotocol/sdk --external @hasna/browser --external @hasna/sandboxes",
93450
93974
  "build:types": "NODE_OPTIONS='--max-old-space-size=8192' tsc --emitDeclarationOnly --outDir dist --skipLibCheck || true",
93451
93975
  "build:dashboard": "cd dashboard && bun run build",
93452
93976
  "build:ext": "cd extension && bun run build",
@@ -93460,10 +93984,11 @@ var package_default = {
93460
93984
  },
93461
93985
  dependencies: {
93462
93986
  "@anthropic-ai/sdk": "^0.52.0",
93463
- "@hasna/browser": "^0.4.5",
93987
+ "@hasna/browser": "^0.4.12",
93464
93988
  "@hasna/cloud": "^0.1.24",
93465
93989
  "@hasna/contacts": "^0.6.8",
93466
93990
  "@hasna/projects": "^0.1.42",
93991
+ "@hasna/sandboxes": "^0.1.27",
93467
93992
  "@modelcontextprotocol/sdk": "^1.12.1",
93468
93993
  ai: "^6.0.175",
93469
93994
  chalk: "^5.4.1",
@@ -93516,9 +94041,9 @@ init_todos_connector();
93516
94041
  init_browser();
93517
94042
  import { render, Box, Text, useInput, useApp } from "ink";
93518
94043
  import React, { useState } from "react";
93519
- import { readFileSync as readFileSync11, readdirSync as readdirSync6, writeFileSync as writeFileSync7 } from "fs";
94044
+ import { readFileSync as readFileSync10, readdirSync as readdirSync6, writeFileSync as writeFileSync8 } from "fs";
93520
94045
  import { createInterface } from "readline";
93521
- import { join as join19, resolve as resolve5 } from "path";
94046
+ import { join as join20, resolve as resolve4 } from "path";
93522
94047
 
93523
94048
  // src/lib/init.ts
93524
94049
  init_paths();
@@ -95247,9 +95772,13 @@ init_flows();
95247
95772
  init_workflows();
95248
95773
 
95249
95774
  // src/lib/workflow-runner.ts
95775
+ init_database();
95250
95776
  init_workflows();
95251
95777
  init_personas();
95252
95778
  init_runner();
95779
+ import { mkdtempSync, rmSync, writeFileSync as writeFileSync4 } from "fs";
95780
+ import { tmpdir } from "os";
95781
+ import { join as join16 } from "path";
95253
95782
  function buildWorkflowRunPlan(workflow, options) {
95254
95783
  const runOptions = {
95255
95784
  url: options.url,
@@ -95266,10 +95795,10 @@ function buildWorkflowRunPlan(workflow, options) {
95266
95795
  return {
95267
95796
  workflow,
95268
95797
  runOptions,
95269
- connectorCommand: workflow.execution.target === "connector:e2b" ? buildConnectorCommand(workflow.execution, runOptions) : null
95798
+ sandbox: workflow.execution.target === "sandbox" ? buildSandboxPlan(workflow, workflow.execution, runOptions) : null
95270
95799
  };
95271
95800
  }
95272
- async function runTestingWorkflow(workflowId, options) {
95801
+ async function runTestingWorkflow(workflowId, options, dependencies = {}) {
95273
95802
  const workflow = getTestingWorkflow(workflowId);
95274
95803
  if (!workflow)
95275
95804
  throw new Error(`Testing workflow not found: ${workflowId}`);
@@ -95279,13 +95808,25 @@ async function runTestingWorkflow(workflowId, options) {
95279
95808
  const plan = buildWorkflowRunPlan(workflow, options);
95280
95809
  if (options.dryRun)
95281
95810
  return { run: null, results: [], plan };
95282
- if (workflow.execution.target === "connector:e2b") {
95283
- const connectorResult = await runViaConnector(plan);
95284
- return { run: null, results: [], plan, connectorResult };
95811
+ if (workflow.execution.target === "sandbox") {
95812
+ const sandboxResult = await runViaSandbox(plan, dependencies);
95813
+ return { run: null, results: [], plan, sandboxResult };
95285
95814
  }
95286
- const { run, results } = await runByFilter(plan.runOptions);
95815
+ const runLocal = dependencies.runByFilter ?? runByFilter;
95816
+ const { run, results } = await runLocal(plan.runOptions);
95287
95817
  return { run, results, plan };
95288
95818
  }
95819
+ function createWorkflowDatabaseBundle(workflow, plan) {
95820
+ if (!plan.sandbox)
95821
+ throw new Error(`Workflow is not configured for sandbox execution: ${workflow.name}`);
95822
+ const localDir = mkdtempSync(join16(tmpdir(), `testers-workflow-${workflow.id.slice(0, 8)}-`));
95823
+ writeFileSync4(join16(localDir, "testers.db"), getDatabase().serialize());
95824
+ return {
95825
+ localDir,
95826
+ remoteDir: plan.sandbox.stateRemoteDir,
95827
+ cleanup: () => rmSync(localDir, { recursive: true, force: true })
95828
+ };
95829
+ }
95289
95830
  function validatePersonaIds(workflow) {
95290
95831
  for (const personaId of workflow.personaIds) {
95291
95832
  if (!getPersona(personaId)) {
@@ -95293,46 +95834,109 @@ function validatePersonaIds(workflow) {
95293
95834
  }
95294
95835
  }
95295
95836
  }
95296
- function buildConnectorCommand(execution, runOptions) {
95297
- const connector = execution.connector ?? "e2b";
95298
- const operation = execution.operation ?? "run";
95299
- const payload = JSON.stringify({
95300
- operation,
95301
- template: execution.sandboxTemplate,
95837
+ function buildSandboxPlan(workflow, execution, runOptions) {
95838
+ const remoteDir = execution.sandboxRemoteDir ?? `/tmp/testers-workflow-${workflow.id.slice(0, 8)}`;
95839
+ const stateRemoteDir = `${remoteDir.replace(/\/+$/, "")}/.testers-state`;
95840
+ return {
95841
+ provider: execution.provider,
95842
+ image: execution.sandboxImage,
95843
+ name: `testers-${workflow.id.slice(0, 8)}`,
95844
+ remoteDir,
95845
+ stateRemoteDir,
95846
+ cleanup: execution.sandboxCleanup ?? "delete",
95302
95847
  timeoutMs: execution.timeoutMs,
95303
- env: execution.env ?? {},
95304
- command: [
95305
- "bunx",
95306
- "@hasna/testers",
95307
- "run",
95308
- runOptions.url,
95309
- ...runOptions.scenarioIds?.length ? ["--scenario", runOptions.scenarioIds.join(",")] : [],
95310
- ...runOptions.tags?.length ? runOptions.tags.flatMap((tag) => ["--tag", tag]) : [],
95311
- ...runOptions.priority ? ["--priority", runOptions.priority] : [],
95312
- ...runOptions.projectId ? ["--project", runOptions.projectId] : [],
95313
- ...runOptions.model ? ["--model", runOptions.model] : [],
95314
- "--json"
95315
- ]
95316
- });
95317
- return ["connectors", "run", connector, operation, payload];
95848
+ env: execution.env,
95849
+ command: buildSandboxCommand({
95850
+ runOptions,
95851
+ remoteDir,
95852
+ dbPath: `${stateRemoteDir}/testers.db`,
95853
+ setupCommand: execution.setupCommand,
95854
+ packageSpec: execution.packageSpec ?? "@hasna/testers"
95855
+ })
95856
+ };
95318
95857
  }
95319
- async function runViaConnector(plan) {
95320
- if (!plan.connectorCommand)
95321
- throw new Error("Workflow does not have a connector command");
95322
- const proc = Bun.spawn(plan.connectorCommand, {
95323
- stdout: "pipe",
95324
- stderr: "pipe",
95325
- env: process.env
95326
- });
95327
- const [stdout, stderr, exitCode] = await Promise.all([
95328
- new Response(proc.stdout).text(),
95329
- new Response(proc.stderr).text(),
95330
- proc.exited
95331
- ]);
95332
- if (exitCode !== 0) {
95333
- throw new Error(`Connector execution failed (${exitCode}): ${stderr || stdout}`);
95858
+ function buildSandboxCommand(input) {
95859
+ const args = [
95860
+ "bunx",
95861
+ input.packageSpec,
95862
+ "run",
95863
+ input.runOptions.url,
95864
+ ...input.runOptions.scenarioIds?.length ? ["--scenario", input.runOptions.scenarioIds.join(",")] : [],
95865
+ ...input.runOptions.tags?.length ? input.runOptions.tags.flatMap((tag) => ["--tag", tag]) : [],
95866
+ ...input.runOptions.priority ? ["--priority", input.runOptions.priority] : [],
95867
+ ...input.runOptions.projectId ? ["--project", input.runOptions.projectId] : [],
95868
+ ...input.runOptions.model ? ["--model", input.runOptions.model] : [],
95869
+ ...input.runOptions.headed ? ["--headed"] : [],
95870
+ ...input.runOptions.parallel ? ["--parallel", String(input.runOptions.parallel)] : [],
95871
+ ...input.runOptions.timeout ? ["--timeout", String(input.runOptions.timeout)] : [],
95872
+ ...input.runOptions.personaIds?.length ? ["--persona", input.runOptions.personaIds.join(",")] : [],
95873
+ "--no-auto-generate",
95874
+ "--json"
95875
+ ];
95876
+ return [
95877
+ "set -euo pipefail",
95878
+ `mkdir -p ${shellQuote(input.remoteDir)}`,
95879
+ `cd ${shellQuote(input.remoteDir)}`,
95880
+ input.setupCommand,
95881
+ `HASNA_TESTERS_DB_PATH=${shellQuote(input.dbPath)} ${args.map(shellQuote).join(" ")}`
95882
+ ].filter(Boolean).join(`
95883
+ `);
95884
+ }
95885
+ async function runViaSandbox(plan, dependencies) {
95886
+ if (!plan.sandbox)
95887
+ throw new Error("Workflow does not have a sandbox plan");
95888
+ const sandboxes = await resolveSandboxesRuntime(dependencies);
95889
+ const createBundle = dependencies.createDatabaseBundle ?? createWorkflowDatabaseBundle;
95890
+ const bundle = createBundle(plan.workflow, plan);
95891
+ try {
95892
+ const raw = await sandboxes.runCommandInSandbox({
95893
+ command: plan.sandbox.command,
95894
+ provider: plan.sandbox.provider,
95895
+ name: plan.sandbox.name,
95896
+ image: plan.sandbox.image,
95897
+ sandboxTimeout: plan.sandbox.timeoutMs,
95898
+ commandTimeoutMs: plan.sandbox.timeoutMs,
95899
+ projectId: plan.workflow.projectId ?? undefined,
95900
+ config: {
95901
+ source: "testers",
95902
+ workflowId: plan.workflow.id,
95903
+ workflowName: plan.workflow.name
95904
+ },
95905
+ sandboxEnvVars: plan.sandbox.env,
95906
+ cleanup: plan.sandbox.cleanup,
95907
+ upload: {
95908
+ localDir: bundle.localDir,
95909
+ remoteDir: bundle.remoteDir
95910
+ }
95911
+ });
95912
+ const exitCode = raw.result.exit_code ?? raw.result.exitCode ?? 0;
95913
+ const stdout = raw.result.stdout ?? "";
95914
+ const stderr = raw.result.stderr ?? "";
95915
+ if (exitCode !== 0) {
95916
+ throw new Error(`Sandbox workflow execution failed (${exitCode}): ${stderr || stdout}`);
95917
+ }
95918
+ return {
95919
+ sandboxId: raw.sandbox.id,
95920
+ sessionId: raw.session.id,
95921
+ exitCode,
95922
+ stdout,
95923
+ stderr,
95924
+ cleanup: raw.cleanup
95925
+ };
95926
+ } finally {
95927
+ bundle.cleanup?.();
95334
95928
  }
95335
- return stdout.trim();
95929
+ }
95930
+ async function resolveSandboxesRuntime(dependencies) {
95931
+ if (dependencies.sandboxes)
95932
+ return dependencies.sandboxes;
95933
+ if (dependencies.createSandboxesSDK)
95934
+ return dependencies.createSandboxesSDK();
95935
+ const mod = await import("@hasna/sandboxes");
95936
+ return mod.createSandboxesSDK();
95937
+ }
95938
+ function shellQuote(value) {
95939
+ return `'${value.replaceAll("'", `'"'"'`)}'`;
95336
95940
  }
95337
95941
 
95338
95942
  // src/db/environments.ts
@@ -95405,111 +96009,19 @@ function getDefaultEnvironment() {
95405
96009
 
95406
96010
  // src/cli/index.tsx
95407
96011
  init_ci();
95408
-
95409
- // src/lib/assertions.ts
95410
- function parseAssertionString(str) {
95411
- const trimmed = str.trim();
95412
- if (trimmed === "no-console-errors") {
95413
- return { type: "no_console_errors", description: "No console errors" };
95414
- }
95415
- if (trimmed.startsWith("url:contains:")) {
95416
- const expected = trimmed.slice("url:contains:".length);
95417
- return { type: "url_contains", expected, description: `URL contains "${expected}"` };
95418
- }
95419
- if (trimmed.startsWith("title:contains:")) {
95420
- const expected = trimmed.slice("title:contains:".length);
95421
- return { type: "title_contains", expected, description: `Title contains "${expected}"` };
95422
- }
95423
- if (trimmed.startsWith("count:")) {
95424
- const rest = trimmed.slice("count:".length);
95425
- const eqIdx = rest.indexOf(" eq:");
95426
- if (eqIdx === -1) {
95427
- throw new Error(`Invalid count assertion format: ${str}. Expected "count:<selector> eq:<number>"`);
95428
- }
95429
- const selector = rest.slice(0, eqIdx);
95430
- const expected = parseInt(rest.slice(eqIdx + " eq:".length), 10);
95431
- return { type: "element_count", selector, expected, description: `${selector} count equals ${expected}` };
95432
- }
95433
- if (trimmed.startsWith("text:")) {
95434
- const rest = trimmed.slice("text:".length);
95435
- const containsIdx = rest.indexOf(" contains:");
95436
- const equalsIdx = rest.indexOf(" equals:");
95437
- if (containsIdx !== -1) {
95438
- const selector = rest.slice(0, containsIdx);
95439
- const expected = rest.slice(containsIdx + " contains:".length);
95440
- return { type: "text_contains", selector, expected, description: `${selector} text contains "${expected}"` };
95441
- }
95442
- if (equalsIdx !== -1) {
95443
- const selector = rest.slice(0, equalsIdx);
95444
- const expected = rest.slice(equalsIdx + " equals:".length);
95445
- return { type: "text_equals", selector, expected, description: `${selector} text equals "${expected}"` };
95446
- }
95447
- throw new Error(`Invalid text assertion format: ${str}. Expected "text:<selector> contains:<text>" or "text:<selector> equals:<text>"`);
95448
- }
95449
- if (trimmed.startsWith("selector:")) {
95450
- const rest = trimmed.slice("selector:".length);
95451
- const lastSpace = rest.lastIndexOf(" ");
95452
- if (lastSpace === -1) {
95453
- throw new Error(`Invalid selector assertion format: ${str}. Expected "selector:<selector> visible" or "selector:<selector> not-visible"`);
95454
- }
95455
- const selector = rest.slice(0, lastSpace);
95456
- const action = rest.slice(lastSpace + 1);
95457
- if (action === "visible") {
95458
- return { type: "visible", selector, description: `${selector} is visible` };
95459
- }
95460
- if (action === "not-visible") {
95461
- return { type: "not_visible", selector, description: `${selector} is not visible` };
95462
- }
95463
- throw new Error(`Unknown selector action: "${action}". Expected "visible" or "not-visible"`);
95464
- }
95465
- if (trimmed.startsWith("cookie:exists:")) {
95466
- const name = trimmed.slice("cookie:exists:".length);
95467
- return { type: "cookie_exists", expected: name, description: `Cookie "${name}" exists` };
95468
- }
95469
- if (trimmed.startsWith("cookie:not-exists:")) {
95470
- const name = trimmed.slice("cookie:not-exists:".length);
95471
- return { type: "cookie_not_exists", expected: name, description: `Cookie "${name}" does not exist` };
95472
- }
95473
- if (trimmed.startsWith("cookie:value:")) {
95474
- const valueStr = trimmed.slice("cookie:value:".length);
95475
- return { type: "cookie_value", expected: valueStr, description: `Cookie value is "${valueStr}"` };
95476
- }
95477
- if (trimmed.startsWith("local:exists:")) {
95478
- const key = trimmed.slice("local:exists:".length);
95479
- return { type: "local_storage_exists", expected: key, description: `LocalStorage key "${key}" exists` };
95480
- }
95481
- if (trimmed.startsWith("local:not-exists:")) {
95482
- const key = trimmed.slice("local:not-exists:".length);
95483
- return { type: "local_storage_not_exists", expected: key, description: `LocalStorage key "${key}" does not exist` };
95484
- }
95485
- if (trimmed.startsWith("local:value:")) {
95486
- const valueStr = trimmed.slice("local:value:".length);
95487
- return { type: "local_storage_value", expected: valueStr, description: `LocalStorage value is "${valueStr}"` };
95488
- }
95489
- if (trimmed.startsWith("session:value:")) {
95490
- const valueStr = trimmed.slice("session:value:".length);
95491
- return { type: "session_storage_value", expected: valueStr, description: `SessionStorage value is "${valueStr}"` };
95492
- }
95493
- if (trimmed.startsWith("session:not-exists:")) {
95494
- const key = trimmed.slice("session:not-exists:".length);
95495
- return { type: "session_storage_not_exists", expected: key, description: `SessionStorage key "${key}" does not exist` };
95496
- }
95497
- throw new Error(`Cannot parse assertion: "${str}". See --help for assertion formats.`);
95498
- }
95499
-
95500
- // src/cli/index.tsx
96012
+ init_assertions();
95501
96013
  init_paths();
95502
96014
  init_sessions();
95503
96015
  import { existsSync as existsSync17, mkdirSync as mkdirSync14 } from "fs";
95504
96016
 
95505
96017
  // src/lib/repo-discovery.ts
95506
96018
  init_paths();
95507
- import { existsSync as existsSync14, readFileSync as readFileSync6, readdirSync as readdirSync3, statSync, writeFileSync as writeFileSync4, mkdirSync as mkdirSync11, unlinkSync } from "fs";
96019
+ import { existsSync as existsSync14, readFileSync as readFileSync6, readdirSync as readdirSync3, statSync, writeFileSync as writeFileSync5, mkdirSync as mkdirSync11, unlinkSync } from "fs";
95508
96020
  import { createHash } from "crypto";
95509
- import { join as join16, resolve, relative as relative2 } from "path";
96021
+ import { join as join17, resolve, relative as relative2 } from "path";
95510
96022
  function getCacheDir() {
95511
96023
  const testersDir = getTestersDir();
95512
- const cacheDir = join16(testersDir, "repo-index");
96024
+ const cacheDir = join17(testersDir, "repo-index");
95513
96025
  if (!existsSync14(cacheDir)) {
95514
96026
  mkdirSync11(cacheDir, { recursive: true });
95515
96027
  }
@@ -95519,11 +96031,11 @@ function pathHash(repoPath) {
95519
96031
  return createHash("sha256").update(repoPath).digest("hex").slice(0, 16);
95520
96032
  }
95521
96033
  function getCachePath(repoPath) {
95522
- return join16(getCacheDir(), `${pathHash(repoPath)}.json`);
96034
+ return join17(getCacheDir(), `${pathHash(repoPath)}.json`);
95523
96035
  }
95524
96036
  function isCacheStale(cached, repoPath) {
95525
96037
  for (const spec of cached.specs) {
95526
- const fullPath = join16(repoPath, spec.file);
96038
+ const fullPath = join17(repoPath, spec.file);
95527
96039
  if (!existsSync14(fullPath))
95528
96040
  return true;
95529
96041
  try {
@@ -95535,11 +96047,11 @@ function isCacheStale(cached, repoPath) {
95535
96047
  }
95536
96048
  }
95537
96049
  if (cached.configPath) {
95538
- const configFullPath = join16(repoPath, cached.configPath);
96050
+ const configFullPath = join17(repoPath, cached.configPath);
95539
96051
  if (!existsSync14(configFullPath))
95540
96052
  return true;
95541
96053
  try {
95542
- const stat = statSync(configFullPath);
96054
+ statSync(configFullPath);
95543
96055
  const age = Date.now() - new Date(cached.snapshotAt).getTime();
95544
96056
  if (age > 3600000)
95545
96057
  return true;
@@ -95562,14 +96074,14 @@ function loadCache(repoPath) {
95562
96074
  }
95563
96075
  function saveCache(snapshot) {
95564
96076
  const cachePath = getCachePath(snapshot.repoPath);
95565
- writeFileSync4(cachePath, JSON.stringify(snapshot, null, 2), "utf-8");
96077
+ writeFileSync5(cachePath, JSON.stringify(snapshot, null, 2), "utf-8");
95566
96078
  }
95567
96079
  function detectPackageManager(repoPath) {
95568
96080
  const result = {
95569
- npm: existsSync14(join16(repoPath, "package-lock.json")),
95570
- yarn: existsSync14(join16(repoPath, "yarn.lock")),
95571
- pnpm: existsSync14(join16(repoPath, "pnpm-lock.yaml")),
95572
- bun: existsSync14(join16(repoPath, "bun.lockb")) || existsSync14(join16(repoPath, "bun.lock")),
96081
+ npm: existsSync14(join17(repoPath, "package-lock.json")),
96082
+ yarn: existsSync14(join17(repoPath, "yarn.lock")),
96083
+ pnpm: existsSync14(join17(repoPath, "pnpm-lock.yaml")),
96084
+ bun: existsSync14(join17(repoPath, "bun.lockb")) || existsSync14(join17(repoPath, "bun.lock")),
95573
96085
  preferred: "npm"
95574
96086
  };
95575
96087
  if (result.bun)
@@ -95583,7 +96095,7 @@ function detectPackageManager(repoPath) {
95583
96095
  return result;
95584
96096
  }
95585
96097
  function detectDevScripts(repoPath) {
95586
- const pkgPath = join16(repoPath, "package.json");
96098
+ const pkgPath = join17(repoPath, "package.json");
95587
96099
  if (!existsSync14(pkgPath)) {
95588
96100
  return { dev: null, test: null, seed: null, build: null };
95589
96101
  }
@@ -95610,7 +96122,7 @@ function findPlaywrightConfig(repoPath) {
95610
96122
  "playwright-ct.config.js"
95611
96123
  ];
95612
96124
  for (const name of candidates) {
95613
- if (existsSync14(join16(repoPath, name)))
96125
+ if (existsSync14(join17(repoPath, name)))
95614
96126
  return name;
95615
96127
  }
95616
96128
  return null;
@@ -95619,7 +96131,7 @@ function extractTestGlobPatterns(configPath, repoPath) {
95619
96131
  if (!configPath) {
95620
96132
  return ["**/*.spec.ts", "**/*.spec.js", "**/*.test.ts", "**/*.test.js", "**/e2e/**/*.ts", "**/e2e/**/*.js", "**/tests/**/*.ts", "**/tests/**/*.js"];
95621
96133
  }
95622
- const fullPath = join16(repoPath, configPath);
96134
+ const fullPath = join17(repoPath, configPath);
95623
96135
  let content;
95624
96136
  try {
95625
96137
  content = readFileSync6(fullPath, "utf-8");
@@ -95630,8 +96142,9 @@ function extractTestGlobPatterns(configPath, repoPath) {
95630
96142
  const testDirMatch = content.match(/testDir\s*[:=]\s*['"`]([^'"`]+)['"`]/);
95631
96143
  const testDir = testDirMatch?.[1];
95632
96144
  const testMatchArray = content.match(/testMatch\s*[:=]\s*\[([^\]]+)\]/);
95633
- if (testMatchArray) {
95634
- const items = testMatchArray[1].match(/['"`]([^'"`]+)['"`]/g);
96145
+ const testMatchBody = testMatchArray?.[1];
96146
+ if (testMatchBody) {
96147
+ const items = testMatchBody.match(/['"`]([^'"`]+)['"`]/g);
95635
96148
  if (items) {
95636
96149
  for (const item of items) {
95637
96150
  patterns.push(item.replace(/['"`]/g, ""));
@@ -95639,8 +96152,9 @@ function extractTestGlobPatterns(configPath, repoPath) {
95639
96152
  }
95640
96153
  }
95641
96154
  const testMatchSingle = content.match(/testMatch\s*[:=]\s*['"`]([^'"`]+)['"`]/);
95642
- if (testMatchSingle) {
95643
- patterns.push(testMatchSingle[1]);
96155
+ const singleTestMatch = testMatchSingle?.[1];
96156
+ if (singleTestMatch) {
96157
+ patterns.push(singleTestMatch);
95644
96158
  }
95645
96159
  if (testDir && patterns.length === 0) {
95646
96160
  patterns.push(`${testDir}/**/*.spec.ts`, `${testDir}/**/*.test.ts`, `${testDir}/**/*.spec.js`, `${testDir}/**/*.test.js`);
@@ -95666,7 +96180,7 @@ function findSpecFiles(repoPath, globPatterns) {
95666
96180
  for (const pattern of globPatterns) {
95667
96181
  const dirsToSearch = ["", ".", "tests", "e2e", "test", "__tests__", "specs", "src"];
95668
96182
  for (const dir of dirsToSearch) {
95669
- const searchDir = dir ? join16(repoPath, dir) : repoPath;
96183
+ const searchDir = dir ? join17(repoPath, dir) : repoPath;
95670
96184
  if (!existsSync14(searchDir))
95671
96185
  continue;
95672
96186
  try {
@@ -95700,7 +96214,7 @@ function walkDir(dir) {
95700
96214
  try {
95701
96215
  const entries = readdirSync3(dir, { withFileTypes: true });
95702
96216
  for (const entry of entries) {
95703
- const fullPath = join16(dir, entry.name);
96217
+ const fullPath = join17(dir, entry.name);
95704
96218
  if (entry.isDirectory()) {
95705
96219
  if (entry.name === "node_modules" || entry.name === ".git")
95706
96220
  continue;
@@ -95718,7 +96232,7 @@ function matchesGlob(filePath, pattern) {
95718
96232
  return new RegExp(regex).test(filePath);
95719
96233
  }
95720
96234
  function detectSuggestedUrl(repoPath) {
95721
- const pkgPath = join16(repoPath, "package.json");
96235
+ const pkgPath = join17(repoPath, "package.json");
95722
96236
  if (!existsSync14(pkgPath))
95723
96237
  return null;
95724
96238
  try {
@@ -95738,10 +96252,10 @@ function detectSuggestedUrl(repoPath) {
95738
96252
  return null;
95739
96253
  }
95740
96254
  function checkPlaywrightBrowserInstalled(repoPath) {
95741
- const cacheDir = join16(repoPath, "node_modules", ".cache", "ms-playwright");
96255
+ const cacheDir = join17(repoPath, "node_modules", ".cache", "ms-playwright");
95742
96256
  if (existsSync14(cacheDir))
95743
96257
  return true;
95744
- const globalCache = join16(repoPath, ".cache", "ms-playwright");
96258
+ const globalCache = join17(repoPath, ".cache", "ms-playwright");
95745
96259
  if (existsSync14(globalCache))
95746
96260
  return true;
95747
96261
  return false;
@@ -95758,7 +96272,7 @@ function getInstallCommand(pm) {
95758
96272
  return "bun install";
95759
96273
  }
95760
96274
  }
95761
- function getPlaywrightInstallCommand(pm) {
96275
+ function getPlaywrightInstallCommand(_pm) {
95762
96276
  return "npx playwright install";
95763
96277
  }
95764
96278
  function discoverRepo(opts) {
@@ -95773,7 +96287,7 @@ function discoverRepo(opts) {
95773
96287
  let configRaw = null;
95774
96288
  if (configPath) {
95775
96289
  try {
95776
- configRaw = readFileSync6(join16(repoPath, configPath), "utf-8");
96290
+ configRaw = readFileSync6(join17(repoPath, configPath), "utf-8");
95777
96291
  } catch {
95778
96292
  configRaw = null;
95779
96293
  }
@@ -95782,7 +96296,7 @@ function discoverRepo(opts) {
95782
96296
  const specs = findSpecFiles(repoPath, globPatterns);
95783
96297
  const packageManager = detectPackageManager(repoPath);
95784
96298
  const devScripts = detectDevScripts(repoPath);
95785
- const playwrightInstalled = existsSync14(join16(repoPath, "node_modules", "playwright")) || existsSync14(join16(repoPath, "node_modules", "@playwright", "test"));
96299
+ const playwrightInstalled = existsSync14(join17(repoPath, "node_modules", "playwright")) || existsSync14(join17(repoPath, "node_modules", "@playwright", "test"));
95786
96300
  const browsersInstalled = checkPlaywrightBrowserInstalled(repoPath);
95787
96301
  const configExists = configPath !== null;
95788
96302
  const specsFound = specs.length > 0;
@@ -95851,7 +96365,7 @@ function clearDiscoveryCache(repoPath) {
95851
96365
  } else {
95852
96366
  for (const file of readdirSync3(cacheDir)) {
95853
96367
  if (file.endsWith(".json")) {
95854
- unlinkSync(join16(cacheDir, file));
96368
+ unlinkSync(join17(cacheDir, file));
95855
96369
  }
95856
96370
  }
95857
96371
  }
@@ -95875,10 +96389,10 @@ init_runs();
95875
96389
  init_database();
95876
96390
  init_paths();
95877
96391
  import { execSync as execSync2 } from "child_process";
95878
- import { existsSync as existsSync15, mkdirSync as mkdirSync12, writeFileSync as writeFileSync5 } from "fs";
95879
- import { join as join17 } from "path";
96392
+ import { existsSync as existsSync15, mkdirSync as mkdirSync12, writeFileSync as writeFileSync6 } from "fs";
96393
+ import { join as join18 } from "path";
95880
96394
  function resolvePlaywrightCmd(repoPath) {
95881
- const localPw = join17(repoPath, "node_modules", ".bin", "playwright");
96395
+ const localPw = join18(repoPath, "node_modules", ".bin", "playwright");
95882
96396
  if (existsSync15(localPw)) {
95883
96397
  return [localPw, "test"];
95884
96398
  }
@@ -95897,7 +96411,7 @@ function buildPlaywrightArgs(specFiles, extraArgs = []) {
95897
96411
  }
95898
96412
  function runPlaywright(repoPath, workingDir, specFiles, extraArgs, timeoutMs) {
95899
96413
  const cmd = resolvePlaywrightCmd(repoPath);
95900
- const args = buildPlaywrightArgs(specFiles, extraArgs, workingDir);
96414
+ const args = buildPlaywrightArgs(specFiles, extraArgs);
95901
96415
  const startTime = Date.now();
95902
96416
  try {
95903
96417
  const result = execSync2(`${cmd.join(" ")} ${args.join(" ")}`, {
@@ -95925,7 +96439,7 @@ function runPlaywright(repoPath, workingDir, specFiles, extraArgs, timeoutMs) {
95925
96439
  };
95926
96440
  }
95927
96441
  }
95928
- function parsePlaywrightJsonOutput(stdout, stderr) {
96442
+ function parsePlaywrightJsonOutput(stdout, _stderr) {
95929
96443
  const testResults = [];
95930
96444
  try {
95931
96445
  const obj = JSON.parse(stdout);
@@ -96030,19 +96544,21 @@ async function runRepoTests(opts) {
96030
96544
  const workingDir = opts.snapshot.workingDir;
96031
96545
  const repoPath = snapshot.repoPath;
96032
96546
  const url = opts.url ?? snapshot.suggestedUrl ?? "http://localhost:3000";
96033
- const run = createRun({
96547
+ const initialRun = createRun({
96034
96548
  projectId: opts.projectId,
96035
96549
  url,
96036
96550
  model: opts.model ?? "repo-native",
96037
96551
  headed: false,
96038
- parallel: 1,
96039
- metadata: {
96552
+ parallel: 1
96553
+ });
96554
+ const run = updateRun(initialRun.id, {
96555
+ metadata: JSON.stringify({
96040
96556
  runType: "repo-native",
96041
96557
  repoPath,
96042
96558
  configPath: snapshot.configPath,
96043
96559
  cacheKey: snapshot.cacheKey,
96044
96560
  label: opts.label
96045
- }
96561
+ })
96046
96562
  });
96047
96563
  const specResults = [];
96048
96564
  const startTime = Date.now();
@@ -96072,10 +96588,10 @@ async function runRepoTests(opts) {
96072
96588
  }
96073
96589
  const resultRecord = { id: resultId };
96074
96590
  if (result.stdout || result.stderr) {
96075
- const reportersDir = join17(getTestersDir(), "repo-run-output");
96591
+ const reportersDir = join18(getTestersDir(), "repo-run-output");
96076
96592
  mkdirSync12(reportersDir, { recursive: true });
96077
- const outputFile = join17(reportersDir, `${resultRecord.id}.log`);
96078
- writeFileSync5(outputFile, `=== stdout ===
96593
+ const outputFile = join18(reportersDir, `${resultRecord.id}.log`);
96594
+ writeFileSync6(outputFile, `=== stdout ===
96079
96595
  ${result.stdout}
96080
96596
 
96081
96597
  === stderr ===
@@ -96182,6 +96698,10 @@ function processSyncEnv() {
96182
96698
  // src/cli/index.tsx
96183
96699
  import { jsxDEV } from "react/jsx-dev-runtime";
96184
96700
  var PRIORITIES = ["low", "medium", "high", "critical"];
96701
+ function splitCsvOption(value) {
96702
+ const items = value?.split(",").map((item) => item.trim()).filter(Boolean) ?? [];
96703
+ return items.length > 0 ? items : undefined;
96704
+ }
96185
96705
  function AddForm({ onComplete }) {
96186
96706
  const { exit } = useApp();
96187
96707
  const [state, setState] = useState({
@@ -96455,25 +96975,30 @@ program2.command("prod-debug <target>").description("Create a safe production de
96455
96975
  }, config2.prodDebug);
96456
96976
  const output = opts.json ? JSON.stringify(plan, null, 2) : formatProdDebugPlan(plan);
96457
96977
  if (opts.output) {
96458
- writeFileSync7(resolve5(opts.output), output + `
96978
+ writeFileSync8(resolve4(opts.output), output + `
96459
96979
  `);
96460
96980
  } else {
96461
96981
  log(output);
96462
96982
  }
96463
96983
  });
96464
96984
  var CONFIG_DIR5 = getTestersDir();
96465
- var CONFIG_PATH4 = join19(CONFIG_DIR5, "config.json");
96985
+ var CONFIG_PATH4 = join20(CONFIG_DIR5, "config.json");
96466
96986
  function getActiveProject() {
96467
96987
  try {
96468
96988
  if (existsSync17(CONFIG_PATH4)) {
96469
- const raw = JSON.parse(readFileSync11(CONFIG_PATH4, "utf-8"));
96989
+ const raw = JSON.parse(readFileSync10(CONFIG_PATH4, "utf-8"));
96470
96990
  return raw.activeProject ?? undefined;
96471
96991
  }
96472
96992
  } catch {}
96473
96993
  return;
96474
96994
  }
96475
96995
  function resolveProject2(optProject) {
96476
- return optProject ?? getActiveProject();
96996
+ if (optProject)
96997
+ return optProject;
96998
+ const activeProject = getActiveProject();
96999
+ if (!activeProject)
97000
+ return;
97001
+ return getProject(activeProject) ? activeProject : undefined;
96477
97002
  }
96478
97003
  program2.command("add [name]").alias("create").description("Create a new test scenario (interactive if no name/flags given)").option("-d, --description <text>", "Scenario description", "").option("-s, --steps <step>", "Test step (repeatable)", (val, acc) => {
96479
97004
  acc.push(val);
@@ -96658,7 +97183,7 @@ program2.command("delete <id>").description("Delete a scenario").option("-y, --y
96658
97183
  }
96659
97184
  if (!opts.yes) {
96660
97185
  process.stdout.write(chalk6.yellow(`Delete scenario ${scenario.shortId} "${scenario.name}"? [y/N] `));
96661
- const answer = await new Promise((resolve6) => {
97186
+ const answer = await new Promise((resolve5) => {
96662
97187
  let buf = "";
96663
97188
  process.stdin.setRawMode?.(true);
96664
97189
  process.stdin.resume();
@@ -96668,7 +97193,7 @@ program2.command("delete <id>").description("Delete a scenario").option("-y, --y
96668
97193
  process.stdin.pause();
96669
97194
  process.stdout.write(`
96670
97195
  `);
96671
- resolve6(buf);
97196
+ resolve5(buf);
96672
97197
  });
96673
97198
  });
96674
97199
  if (answer !== "y" && answer !== "yes") {
@@ -96697,7 +97222,7 @@ program2.command("remove <id>").alias("uninstall").description("Remove a scenari
96697
97222
  }
96698
97223
  if (!opts.yes) {
96699
97224
  process.stdout.write(chalk6.yellow(`Remove scenario ${scenario.shortId} "${scenario.name}"? [y/N] `));
96700
- const answer = await new Promise((resolve6) => {
97225
+ const answer = await new Promise((resolve5) => {
96701
97226
  let buf = "";
96702
97227
  process.stdin.setRawMode?.(true);
96703
97228
  process.stdin.resume();
@@ -96707,7 +97232,7 @@ program2.command("remove <id>").alias("uninstall").description("Remove a scenari
96707
97232
  process.stdin.pause();
96708
97233
  process.stdout.write(`
96709
97234
  `);
96710
- resolve6(buf);
97235
+ resolve5(buf);
96711
97236
  });
96712
97237
  });
96713
97238
  if (answer !== "y" && answer !== "yes") {
@@ -96753,6 +97278,7 @@ program2.command("run [url] [description]").alias("test").description("Run test
96753
97278
  logError(chalk6.red("No URL provided. Pass a URL argument, use --env <name>, or set a default environment with 'testers env use <name>'."));
96754
97279
  process.exit(2);
96755
97280
  }
97281
+ const scenarioIds = splitCsvOption(opts.scenario);
96756
97282
  if (!opts.dryRun) {
96757
97283
  const hasAnthropic = Boolean(process.env["ANTHROPIC_API_KEY"]);
96758
97284
  const hasOpenAI = Boolean(process.env["OPENAI_API_KEY"]);
@@ -96823,7 +97349,7 @@ program2.command("run [url] [description]").alias("test").description("Run test
96823
97349
  tags: opts.tag.length > 0 ? opts.tag : undefined,
96824
97350
  projectId
96825
97351
  }).filter((s2) => {
96826
- if (opts.scenario && s2.id !== opts.scenario && s2.shortId !== opts.scenario)
97352
+ if (scenarioIds && !scenarioIds.includes(s2.id) && !scenarioIds.includes(s2.shortId))
96827
97353
  return false;
96828
97354
  if (opts.priority && s2.priority !== opts.priority)
96829
97355
  return false;
@@ -96872,7 +97398,7 @@ program2.command("run [url] [description]").alias("test").description("Run test
96872
97398
  const { runId, scenarioCount } = startRunAsync({
96873
97399
  url: url2,
96874
97400
  tags: opts.tag.length > 0 ? opts.tag : undefined,
96875
- scenarioIds: opts.scenario ? [opts.scenario] : undefined,
97401
+ scenarioIds,
96876
97402
  priority: opts.priority,
96877
97403
  model: opts.model,
96878
97404
  headed: opts.headed,
@@ -96906,7 +97432,7 @@ program2.command("run [url] [description]").alias("test").description("Run test
96906
97432
  `);
96907
97433
  }
96908
97434
  };
96909
- await new Promise((resolve6) => {
97435
+ await new Promise((resolve5) => {
96910
97436
  const poll = setInterval(() => {
96911
97437
  const run3 = getRun(runId);
96912
97438
  if (!run3)
@@ -96914,7 +97440,7 @@ program2.command("run [url] [description]").alias("test").description("Run test
96914
97440
  renderTable();
96915
97441
  if (DONE_STATUSES.has(run3.status)) {
96916
97442
  clearInterval(poll);
96917
- resolve6();
97443
+ resolve5();
96918
97444
  }
96919
97445
  }, POLL_INTERVAL);
96920
97446
  });
@@ -97007,7 +97533,7 @@ program2.command("run [url] [description]").alias("test").description("Run test
97007
97533
  if (opts.json || opts.output) {
97008
97534
  const jsonOutput = formatJSON(run3, results2);
97009
97535
  if (opts.output) {
97010
- writeFileSync7(opts.output, jsonOutput, "utf-8");
97536
+ writeFileSync8(opts.output, jsonOutput, "utf-8");
97011
97537
  log(chalk6.green(`Results written to ${opts.output}`));
97012
97538
  }
97013
97539
  if (opts.json) {
@@ -97030,7 +97556,7 @@ program2.command("run [url] [description]").alias("test").description("Run test
97030
97556
  }
97031
97557
  process.exit(getExitCode(run3));
97032
97558
  }
97033
- const noFilters = !opts.scenario && opts.tag.length === 0 && !opts.priority;
97559
+ const noFilters = !scenarioIds && opts.tag.length === 0 && !opts.priority;
97034
97560
  if (noFilters && !opts.json && !opts.output) {
97035
97561
  const allScenarios = listScenarios({ projectId });
97036
97562
  log(chalk6.bold(` Running all ${allScenarios.length} scenarios...`));
@@ -97094,7 +97620,7 @@ program2.command("run [url] [description]").alias("test").description("Run test
97094
97620
  const { run: run2, results } = await runByFilter({
97095
97621
  url: url2,
97096
97622
  tags: opts.tag.length > 0 ? opts.tag : undefined,
97097
- scenarioIds: diffScenarioIds ?? (opts.scenario ? [opts.scenario] : undefined),
97623
+ scenarioIds: diffScenarioIds ?? scenarioIds,
97098
97624
  priority: opts.priority,
97099
97625
  model: opts.model,
97100
97626
  headed: opts.headed,
@@ -97116,7 +97642,7 @@ program2.command("run [url] [description]").alias("test").description("Run test
97116
97642
  if (opts.json || opts.output) {
97117
97643
  const jsonOutput = formatJSON(run2, results);
97118
97644
  if (opts.output) {
97119
- writeFileSync7(opts.output, jsonOutput, "utf-8");
97645
+ writeFileSync8(opts.output, jsonOutput, "utf-8");
97120
97646
  log(chalk6.green(`Results written to ${opts.output}`));
97121
97647
  }
97122
97648
  if (opts.json) {
@@ -97306,7 +97832,7 @@ program2.command("screenshots <id>").description("List screenshots for a run or
97306
97832
  });
97307
97833
  program2.command("import <dir>").description("Import markdown test files as scenarios").action((dir) => {
97308
97834
  try {
97309
- const absDir = resolve5(dir);
97835
+ const absDir = resolve4(dir);
97310
97836
  const files = readdirSync6(absDir).filter((f2) => f2.endsWith(".md"));
97311
97837
  if (files.length === 0) {
97312
97838
  log(chalk6.dim("No .md files found in directory."));
@@ -97314,7 +97840,7 @@ program2.command("import <dir>").description("Import markdown test files as scen
97314
97840
  }
97315
97841
  let imported = 0;
97316
97842
  for (const file2 of files) {
97317
- const content = readFileSync11(join19(absDir, file2), "utf-8");
97843
+ const content = readFileSync10(join20(absDir, file2), "utf-8");
97318
97844
  const lines = content.split(`
97319
97845
  `);
97320
97846
  let name21 = file2.replace(/\.md$/, "");
@@ -97369,8 +97895,8 @@ program2.command("export [format]").description("Export scenarios as JSON (defau
97369
97895
  if (fmt === "json") {
97370
97896
  const outputPath = opts.output ?? "testers-export.json";
97371
97897
  const data = JSON.stringify(scenarios, null, 2);
97372
- writeFileSync7(outputPath, data, "utf-8");
97373
- log(chalk6.green(`Exported ${scenarios.length} scenario(s) to ${resolve5(outputPath)}`));
97898
+ writeFileSync8(outputPath, data, "utf-8");
97899
+ log(chalk6.green(`Exported ${scenarios.length} scenario(s) to ${resolve4(outputPath)}`));
97374
97900
  return;
97375
97901
  }
97376
97902
  const outputDir = opts.output ?? ".";
@@ -97402,13 +97928,13 @@ program2.command("export [format]").description("Export scenarios as JSON (defau
97402
97928
  lines.push("");
97403
97929
  }
97404
97930
  const safeFilename = s2.name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 80);
97405
- const filePath = join19(outputDir, `${s2.shortId}-${safeFilename}.md`);
97406
- writeFileSync7(filePath, lines.join(`
97931
+ const filePath = join20(outputDir, `${s2.shortId}-${safeFilename}.md`);
97932
+ writeFileSync8(filePath, lines.join(`
97407
97933
  `), "utf-8");
97408
97934
  log(chalk6.dim(` ${s2.shortId}: ${s2.name} \u2192 ${filePath}`));
97409
97935
  }
97410
97936
  log(chalk6.green(`
97411
- Exported ${scenarios.length} scenario(s) as markdown to ${resolve5(outputDir)}`));
97937
+ Exported ${scenarios.length} scenario(s) as markdown to ${resolve4(outputDir)}`));
97412
97938
  } catch (error40) {
97413
97939
  logError(chalk6.red(`Error: ${error40 instanceof Error ? error40.message : String(error40)}`));
97414
97940
  process.exit(1);
@@ -97427,7 +97953,7 @@ program2.command("status").description("Show database and auth status").action((
97427
97953
  try {
97428
97954
  const config2 = loadConfig();
97429
97955
  const hasApiKey = !!config2.anthropicApiKey || !!process.env["ANTHROPIC_API_KEY"];
97430
- const dbPath = join19(getTestersDir(), "testers.db");
97956
+ const dbPath = join20(getTestersDir(), "testers.db");
97431
97957
  log("");
97432
97958
  log(chalk6.bold(" Open Testers Status"));
97433
97959
  log("");
@@ -97581,11 +98107,11 @@ projectCmd.command("use <name>").description("Set active project (find or create
97581
98107
  let config2 = {};
97582
98108
  if (existsSync17(CONFIG_PATH4)) {
97583
98109
  try {
97584
- config2 = JSON.parse(readFileSync11(CONFIG_PATH4, "utf-8"));
98110
+ config2 = JSON.parse(readFileSync10(CONFIG_PATH4, "utf-8"));
97585
98111
  } catch {}
97586
98112
  }
97587
98113
  config2.activeProject = project.id;
97588
- writeFileSync7(CONFIG_PATH4, JSON.stringify(config2, null, 2), "utf-8");
98114
+ writeFileSync8(CONFIG_PATH4, JSON.stringify(config2, null, 2), "utf-8");
97589
98115
  if (opts.json) {
97590
98116
  log(JSON.stringify({ activeProject: project.id, project }, null, 2));
97591
98117
  return;
@@ -97599,7 +98125,7 @@ projectCmd.command("use <name>").description("Set active project (find or create
97599
98125
  var repoCmd = program2.command("repo").description("Discover and run repo-native Playwright tests");
97600
98126
  repoCmd.command("discover [path]").alias("scan").description("Discover Playwright tests in a repo").option("--refresh", "Force a fresh scan, ignoring cache", false).option("--json", "Output as JSON", false).option("--base-url <url>", "Override the suggested base URL").action((path, opts) => {
97601
98127
  try {
97602
- const repoPath = resolve5(path ?? process.cwd());
98128
+ const repoPath = resolve4(path ?? process.cwd());
97603
98129
  const snapshot = discoverRepo({
97604
98130
  repoPath,
97605
98131
  refresh: opts.refresh,
@@ -97665,7 +98191,7 @@ repoCmd.command("discover [path]").alias("scan").description("Discover Playwrigh
97665
98191
  });
97666
98192
  repoCmd.command("prepare [path]").alias("prep").description("Install dependencies and browsers for repo tests").option("--all", "Run all prep steps (install, browsers, build, seed)", false).option("--install", "Install dependencies", false).option("--browsers", "Install Playwright browsers", false).option("--build", "Build the app", false).option("--seed", "Seed the database", false).option("--refresh", "Force fresh discovery scan", false).option("--json", "Output as JSON", false).action((path, opts) => {
97667
98193
  try {
97668
- const repoPath = resolve5(path ?? process.cwd());
98194
+ const repoPath = resolve4(path ?? process.cwd());
97669
98195
  const snapshot = discoverRepo({ repoPath, refresh: opts.refresh });
97670
98196
  const steps = [];
97671
98197
  if (opts.all) {
@@ -97743,7 +98269,7 @@ repoCmd.command("run [path]").description("Run discovered Playwright tests nativ
97743
98269
  return acc;
97744
98270
  }, []).option("--timeout <ms>", "Timeout per spec file", "300000").option("--url <url>", "Dev server URL").option("--project <id>", "Project ID for result storage").option("--label <text>", "Run label").option("--json", "Output as JSON", false).action(async (path, opts) => {
97745
98271
  try {
97746
- const repoPath = resolve5(path ?? process.cwd());
98272
+ const repoPath = resolve4(path ?? process.cwd());
97747
98273
  const snapshot = discoverRepo({
97748
98274
  repoPath,
97749
98275
  refresh: opts.refresh,
@@ -97809,7 +98335,7 @@ repoCmd.command("run [path]").description("Run discovered Playwright tests nativ
97809
98335
  repoCmd.command("cache [path]").description("Manage discovery cache").option("--clear", "Clear discovery cache", false).option("--status", "Show cache status", false).action((path, opts) => {
97810
98336
  try {
97811
98337
  if (opts.clear) {
97812
- const repoPath2 = path ? resolve5(path) : undefined;
98338
+ const repoPath2 = path ? resolve4(path) : undefined;
97813
98339
  clearDiscoveryCache(repoPath2);
97814
98340
  if (repoPath2) {
97815
98341
  log(chalk6.green("Discovery cache cleared for this repo."));
@@ -97819,7 +98345,7 @@ repoCmd.command("cache [path]").description("Manage discovery cache").option("--
97819
98345
  return;
97820
98346
  }
97821
98347
  if (opts.status) {
97822
- const repoPath2 = resolve5(path ?? process.cwd());
98348
+ const repoPath2 = resolve4(path ?? process.cwd());
97823
98349
  const info2 = getDiscoveryCacheInfo(repoPath2);
97824
98350
  if (!info2) {
97825
98351
  log(chalk6.dim("No discovery cache for this repo."));
@@ -97833,7 +98359,7 @@ repoCmd.command("cache [path]").description("Manage discovery cache").option("--
97833
98359
  log("");
97834
98360
  return;
97835
98361
  }
97836
- const repoPath = resolve5(path ?? process.cwd());
98362
+ const repoPath = resolve4(path ?? process.cwd());
97837
98363
  const info = getDiscoveryCacheInfo(repoPath);
97838
98364
  if (!info) {
97839
98365
  log(chalk6.dim("No discovery cache. Run 'testers repo discover' to create one."));
@@ -97922,8 +98448,8 @@ sessionCmd.command("show <id>").description("Show details of a recorded session"
97922
98448
  });
97923
98449
  sessionCmd.command("import <file>").description("Import a session JSON file exported from the Chrome extension").action(async (file2) => {
97924
98450
  try {
97925
- const { readFileSync: readFileSync12 } = await import("fs");
97926
- const raw = readFileSync12(file2, "utf-8");
98451
+ const { readFileSync: readFileSync11 } = await import("fs");
98452
+ const raw = readFileSync11(file2, "utf-8");
97927
98453
  const data = JSON.parse(raw);
97928
98454
  const items = Array.isArray(data) ? data : [data];
97929
98455
  const { createSession: createSession2 } = await Promise.resolve().then(() => (init_sessions(), exports_sessions));
@@ -98163,7 +98689,7 @@ program2.command("daemon").description("Start the scheduler daemon").option("--i
98163
98689
  } catch (err) {
98164
98690
  logError(chalk6.red(`Daemon error: ${err instanceof Error ? err.message : String(err)}`));
98165
98691
  }
98166
- await new Promise((resolve6) => setTimeout(resolve6, intervalMs));
98692
+ await new Promise((resolve5) => setTimeout(resolve5, intervalMs));
98167
98693
  }
98168
98694
  };
98169
98695
  process.on("SIGINT", () => {
@@ -98192,12 +98718,12 @@ program2.command("ci [provider]").description("Print or write a CI workflow (def
98192
98718
  }
98193
98719
  const workflow = generateGitHubActionsWorkflow();
98194
98720
  if (opts.output) {
98195
- const outPath = resolve5(opts.output);
98721
+ const outPath = resolve4(opts.output);
98196
98722
  const outDir = outPath.replace(/\/[^/]*$/, "");
98197
98723
  if (outDir && !existsSync17(outDir)) {
98198
98724
  mkdirSync14(outDir, { recursive: true });
98199
98725
  }
98200
- writeFileSync7(outPath, workflow, "utf-8");
98726
+ writeFileSync8(outPath, workflow, "utf-8");
98201
98727
  log(chalk6.green(`Workflow written to ${outPath}`));
98202
98728
  return;
98203
98729
  }
@@ -98228,12 +98754,12 @@ program2.command("init").description("Initialize a new testing project").option(
98228
98754
  log(` ${chalk6.dim(s2.shortId)} ${s2.name} ${chalk6.dim(`[${s2.tags.join(", ")}]`)}`);
98229
98755
  }
98230
98756
  if (opts.ci === "github") {
98231
- const workflowDir = join19(process.cwd(), ".github", "workflows");
98757
+ const workflowDir = join20(process.cwd(), ".github", "workflows");
98232
98758
  if (!existsSync17(workflowDir)) {
98233
98759
  mkdirSync14(workflowDir, { recursive: true });
98234
98760
  }
98235
- const workflowPath = join19(workflowDir, "testers.yml");
98236
- writeFileSync7(workflowPath, generateGitHubActionsWorkflow(), "utf-8");
98761
+ const workflowPath = join20(workflowDir, "testers.yml");
98762
+ writeFileSync8(workflowPath, generateGitHubActionsWorkflow(), "utf-8");
98237
98763
  log(` CI: ${chalk6.green("GitHub Actions workflow written to .github/workflows/testers.yml")}`);
98238
98764
  } else if (opts.ci) {
98239
98765
  log(chalk6.yellow(` Unknown CI provider: ${opts.ci}. Supported: github`));
@@ -98242,7 +98768,7 @@ program2.command("init").description("Initialize a new testing project").option(
98242
98768
  if (opts.yes)
98243
98769
  return;
98244
98770
  const rl2 = createInterface({ input: process.stdin, output: process.stdout });
98245
- const ask = (q2) => new Promise((resolve6) => rl2.question(q2, resolve6));
98771
+ const ask = (q2) => new Promise((resolve5) => rl2.question(q2, resolve5));
98246
98772
  try {
98247
98773
  const envAnswer = await ask(" Would you like to configure environments? [y/N] ");
98248
98774
  if (envAnswer.trim().toLowerCase() === "y") {
@@ -98427,8 +98953,8 @@ program2.command("report [run-id]").description("Generate HTML test report or co
98427
98953
  format
98428
98954
  });
98429
98955
  if (opts.output && opts.output !== "report.html") {
98430
- writeFileSync7(opts.output, content, "utf-8");
98431
- const absPath2 = resolve5(opts.output);
98956
+ writeFileSync8(opts.output, content, "utf-8");
98957
+ const absPath2 = resolve4(opts.output);
98432
98958
  log(chalk6.green(`Compliance report written to ${absPath2}`));
98433
98959
  } else {
98434
98960
  log(content);
@@ -98441,8 +98967,8 @@ program2.command("report [run-id]").description("Generate HTML test report or co
98441
98967
  } else {
98442
98968
  html = generateHtmlReport(runId);
98443
98969
  }
98444
- writeFileSync7(opts.output, html, "utf-8");
98445
- const absPath = resolve5(opts.output);
98970
+ writeFileSync8(opts.output, html, "utf-8");
98971
+ const absPath = resolve4(opts.output);
98446
98972
  log(chalk6.green(`Report generated: ${absPath}`));
98447
98973
  if (opts.open) {
98448
98974
  const openCmd = process.platform === "darwin" ? "open" : "xdg-open";
@@ -99251,7 +99777,7 @@ program2.command("doctor").description("Check system setup and configuration").a
99251
99777
  log(chalk6.red("\u2717") + " ANTHROPIC_API_KEY is not set (required for AI-powered tests)");
99252
99778
  allPassed = false;
99253
99779
  }
99254
- const dbPath = join19(getTestersDir(), "testers.db");
99780
+ const dbPath = join20(getTestersDir(), "testers.db");
99255
99781
  try {
99256
99782
  const { Database: Database5 } = await import("bun:sqlite");
99257
99783
  const db2 = new Database5(dbPath, { create: true });
@@ -99303,7 +99829,7 @@ program2.command("serve").description("Start the Open Testers web dashboard").op
99303
99829
  try {
99304
99830
  const port = parseInt(opts.port, 10);
99305
99831
  const url2 = `http://localhost:${port}`;
99306
- const serverBin = join19(resolve5(process.execPath, ".."), "..", "dist", "server", "index.js");
99832
+ const serverBin = join20(resolve4(process.execPath, ".."), "..", "dist", "server", "index.js");
99307
99833
  const { join: pathJoin, resolve: pathResolve, dirname: dirname7 } = await import("path");
99308
99834
  const { fileURLToPath: fileURLToPath2 } = await import("url");
99309
99835
  const serverPath = pathJoin(dirname7(fileURLToPath2(import.meta.url)), "..", "server", "index.js");
@@ -99719,7 +100245,7 @@ workflowCmd.command("create <name>").description("Save a reusable testing workfl
99719
100245
  }, []).option("--priority <level>", "Scenario priority").option("--persona <ids>", "Comma-separated persona IDs").option("--goal <prompt>", "Goal prompt for the agentic testing loop").option("--success <criteria>", "Success criteria (repeatable)", (val, acc) => {
99720
100246
  acc.push(val);
99721
100247
  return acc;
99722
- }, []).option("--max-iterations <n>", "Goal-loop iteration cap", "10").option("--target <target>", "Execution target: local or connector:e2b", "local").option("--e2b-template <name>", "E2B sandbox template name").option("--connector-operation <name>", "Connector operation for E2B", "run").option("--timeout <ms>", "Workflow timeout").option("--json", "Output as JSON", false).action((name21, opts) => {
100248
+ }, []).option("--max-iterations <n>", "Goal-loop iteration cap", "10").option("--target <target>", "Execution target: local or sandbox", "local").option("--sandbox-provider <provider>", "Sandbox provider: e2b, daytona, or modal").option("--sandbox-image <image>", "Sandbox image/template").option("--sandbox-remote-dir <path>", "Remote working directory for sandbox runs").option("--sandbox-cleanup <mode>", "Sandbox cleanup mode: delete, stop, or keep", "delete").option("--sandbox-setup-command <command>", "Shell command to run before testers in the sandbox").option("--sandbox-package <spec>", "Package spec to execute in the sandbox", "@hasna/testers").option("--e2b-template <name>", "Legacy alias for --sandbox-image").option("--timeout <ms>", "Workflow timeout").option("--json", "Output as JSON", false).action((name21, opts) => {
99723
100249
  try {
99724
100250
  const workflow = createTestingWorkflow({
99725
100251
  name: name21,
@@ -99738,9 +100264,12 @@ workflowCmd.command("create <name>").description("Save a reusable testing workfl
99738
100264
  } : null,
99739
100265
  execution: {
99740
100266
  target: opts.target,
99741
- connector: opts.target === "connector:e2b" ? "e2b" : undefined,
99742
- operation: opts.connectorOperation,
99743
- sandboxTemplate: opts.e2bTemplate,
100267
+ provider: opts.sandboxProvider ?? (opts.target === "connector:e2b" ? "e2b" : undefined),
100268
+ sandboxImage: opts.sandboxImage ?? opts.e2bTemplate,
100269
+ sandboxRemoteDir: opts.sandboxRemoteDir,
100270
+ sandboxCleanup: opts.sandboxCleanup,
100271
+ setupCommand: opts.sandboxSetupCommand,
100272
+ packageSpec: opts.sandboxPackage,
99744
100273
  timeoutMs: opts.timeout ? parseInt(opts.timeout, 10) : undefined
99745
100274
  }
99746
100275
  });
@@ -99772,7 +100301,7 @@ workflowCmd.command("list").description("List saved testing workflows").option("
99772
100301
  log(chalk6.bold(" Testing Workflows"));
99773
100302
  log("");
99774
100303
  for (const workflow of workflows) {
99775
- const target = workflow.execution.target === "connector:e2b" ? chalk6.cyan("e2b") : chalk6.green("local");
100304
+ const target = workflow.execution.target === "sandbox" ? chalk6.cyan(`sandbox${workflow.execution.provider ? `:${workflow.execution.provider}` : ""}`) : chalk6.green("local");
99776
100305
  log(` ${chalk6.dim(workflow.id.slice(0, 8))} ${workflow.name} ${target} ${chalk6.dim(workflow.personaIds.length ? `${workflow.personaIds.length} personas` : "no personas")}`);
99777
100306
  }
99778
100307
  log("");
@@ -99984,7 +100513,7 @@ personaCmd.command("delete <id>").description("Delete a persona").option("-y, --
99984
100513
  }
99985
100514
  if (!opts.yes) {
99986
100515
  process.stdout.write(chalk6.yellow(`Delete persona ${persona.shortId} "${persona.name}"? [y/N] `));
99987
- const answer = await new Promise((resolve6) => {
100516
+ const answer = await new Promise((resolve5) => {
99988
100517
  let buf = "";
99989
100518
  process.stdin.setRawMode?.(true);
99990
100519
  process.stdin.resume();
@@ -99994,7 +100523,7 @@ personaCmd.command("delete <id>").description("Delete a persona").option("-y, --
99994
100523
  process.stdin.pause();
99995
100524
  process.stdout.write(`
99996
100525
  `);
99997
- resolve6(buf);
100526
+ resolve5(buf);
99998
100527
  });
99999
100528
  });
100000
100529
  if (answer !== "y" && answer !== "yes") {
@@ -100155,7 +100684,7 @@ evalCmd.command("rag <url>").description("Run RAG quality evaluation \u2014 fait
100155
100684
  let ragTestCases = [];
100156
100685
  if (opts.docs) {
100157
100686
  try {
100158
- const raw = readFileSync11(opts.docs, "utf-8");
100687
+ const raw = readFileSync10(opts.docs, "utf-8");
100159
100688
  ragTestCases = JSON.parse(raw);
100160
100689
  } catch {
100161
100690
  logError(chalk6.red(`Failed to read docs file: ${opts.docs}`));
@@ -100245,9 +100774,9 @@ Created golden answer check ${chalk6.bold(golden2.shortId)}`));
100245
100774
  }
100246
100775
  const ask = (prompt) => {
100247
100776
  const rl2 = createInterface({ input: process.stdin, output: process.stdout });
100248
- return new Promise((resolve6) => rl2.question(prompt, (ans) => {
100777
+ return new Promise((resolve5) => rl2.question(prompt, (ans) => {
100249
100778
  rl2.close();
100250
- resolve6(ans.trim());
100779
+ resolve5(ans.trim());
100251
100780
  }));
100252
100781
  };
100253
100782
  const question = await ask("Question (what this endpoint should answer): ");
@@ -100408,9 +100937,9 @@ program2.command("run-many <url>").description("Run scenarios \xD7 personas matr
100408
100937
  });
100409
100938
  program2.command("run-script <file>").description("Run a hybrid test script (.ts) that exports an array of HybridScenario objects").option("--url <url>", "Base URL to run against").option("--json", "Output as JSON", false).action(async (file2, opts) => {
100410
100939
  try {
100411
- const { resolve: resolve6 } = await import("path");
100940
+ const { resolve: resolve5 } = await import("path");
100412
100941
  const { runHybridScenario: runHybridScenario2 } = await Promise.resolve().then(() => (init_hybrid_runner(), exports_hybrid_runner));
100413
- const scriptPath = resolve6(process.cwd(), file2);
100942
+ const scriptPath = resolve5(process.cwd(), file2);
100414
100943
  const mod = await import(scriptPath);
100415
100944
  const scenarios = mod.scenarios ?? mod.default ?? [];
100416
100945
  if (!Array.isArray(scenarios) || scenarios.length === 0) {