@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/mcp/index.js CHANGED
@@ -52,7 +52,7 @@ var package_default;
52
52
  var init_package = __esm(() => {
53
53
  package_default = {
54
54
  name: "@hasna/testers",
55
- version: "0.0.33",
55
+ version: "0.0.35",
56
56
  description: "AI-powered QA testing CLI \u2014 spawns cheap AI agents to test web apps with headless browsers",
57
57
  type: "module",
58
58
  main: "dist/index.js",
@@ -76,10 +76,10 @@ var init_package = __esm(() => {
76
76
  ],
77
77
  scripts: {
78
78
  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",
79
- "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",
80
- "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",
81
- "build:server": "bun build src/server/index.ts --outdir dist/server --target bun --external @anthropic-ai/sdk --external playwright --external @hasna/browser",
82
- "build:lib": "bun build src/index.ts --outdir dist --target bun --external playwright --external @anthropic-ai/sdk --external @modelcontextprotocol/sdk --external @hasna/browser",
79
+ "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",
80
+ "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",
81
+ "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",
82
+ "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",
83
83
  "build:types": "NODE_OPTIONS='--max-old-space-size=8192' tsc --emitDeclarationOnly --outDir dist --skipLibCheck || true",
84
84
  "build:dashboard": "cd dashboard && bun run build",
85
85
  "build:ext": "cd extension && bun run build",
@@ -93,10 +93,11 @@ var init_package = __esm(() => {
93
93
  },
94
94
  dependencies: {
95
95
  "@anthropic-ai/sdk": "^0.52.0",
96
- "@hasna/browser": "^0.4.5",
96
+ "@hasna/browser": "^0.4.12",
97
97
  "@hasna/cloud": "^0.1.24",
98
98
  "@hasna/contacts": "^0.6.8",
99
99
  "@hasna/projects": "^0.1.42",
100
+ "@hasna/sandboxes": "^0.1.27",
100
101
  "@modelcontextprotocol/sdk": "^1.12.1",
101
102
  ai: "^6.0.175",
102
103
  chalk: "^5.4.1",
@@ -14134,6 +14135,56 @@ See https://www.postgresql.org/docs/current/libpq-ssl.html for libpq SSL mode de
14134
14135
  });
14135
14136
 
14136
14137
  // src/types/index.ts
14138
+ function isRecord(value) {
14139
+ return typeof value === "object" && value !== null && !Array.isArray(value);
14140
+ }
14141
+ function stringValue(value) {
14142
+ return typeof value === "string" && value.trim() ? value : undefined;
14143
+ }
14144
+ function numberValue(value) {
14145
+ return typeof value === "number" && Number.isFinite(value) ? value : undefined;
14146
+ }
14147
+ function stringMap(value) {
14148
+ if (!isRecord(value))
14149
+ return;
14150
+ const entries = Object.entries(value).filter((entry) => typeof entry[1] === "string");
14151
+ return entries.length > 0 ? Object.fromEntries(entries) : undefined;
14152
+ }
14153
+ function cleanupValue(value) {
14154
+ if (value === "delete" || value === "stop" || value === "keep")
14155
+ return value;
14156
+ return;
14157
+ }
14158
+ function workflowExecutionFromValue(value) {
14159
+ const input = isRecord(value) ? value : {};
14160
+ const rawTarget = stringValue(input["target"]) ?? "local";
14161
+ if (rawTarget === "local") {
14162
+ const timeoutMs2 = numberValue(input["timeoutMs"]);
14163
+ return timeoutMs2 === undefined ? { target: "local" } : { target: "local", timeoutMs: timeoutMs2 };
14164
+ }
14165
+ if (rawTarget !== "sandbox" && rawTarget !== "connector:e2b") {
14166
+ throw new Error(`Unsupported workflow execution target: ${rawTarget}`);
14167
+ }
14168
+ const provider = rawTarget === "connector:e2b" ? "e2b" : stringValue(input["provider"]) ?? stringValue(input["connector"]);
14169
+ const sandboxImage = stringValue(input["sandboxImage"]) ?? stringValue(input["sandboxTemplate"]);
14170
+ const sandboxRemoteDir = stringValue(input["sandboxRemoteDir"]);
14171
+ const sandboxCleanup = cleanupValue(input["sandboxCleanup"]);
14172
+ const setupCommand = stringValue(input["setupCommand"]);
14173
+ const packageSpec = stringValue(input["packageSpec"]);
14174
+ const timeoutMs = numberValue(input["timeoutMs"]);
14175
+ const env = stringMap(input["env"]);
14176
+ return {
14177
+ target: "sandbox",
14178
+ ...provider ? { provider } : {},
14179
+ ...sandboxImage ? { sandboxImage } : {},
14180
+ ...sandboxRemoteDir ? { sandboxRemoteDir } : {},
14181
+ ...sandboxCleanup ? { sandboxCleanup } : {},
14182
+ ...setupCommand ? { setupCommand } : {},
14183
+ ...packageSpec ? { packageSpec } : {},
14184
+ ...timeoutMs !== undefined ? { timeoutMs } : {},
14185
+ ...env ? { env } : {}
14186
+ };
14187
+ }
14137
14188
  function workflowFromRow(row) {
14138
14189
  return {
14139
14190
  id: row.id,
@@ -14143,7 +14194,7 @@ function workflowFromRow(row) {
14143
14194
  scenarioFilter: JSON.parse(row.scenario_filter || "{}"),
14144
14195
  personaIds: JSON.parse(row.persona_ids || "[]"),
14145
14196
  goal: row.goal ? JSON.parse(row.goal) : null,
14146
- execution: JSON.parse(row.execution || '{"target":"local"}'),
14197
+ execution: workflowExecutionFromValue(JSON.parse(row.execution || '{"target":"local"}')),
14147
14198
  settings: JSON.parse(row.settings || "{}"),
14148
14199
  enabled: row.enabled === 1,
14149
14200
  createdAt: row.created_at,
@@ -16696,6 +16747,7 @@ __export(exports_ai_client, {
16696
16747
  createClientForModel: () => createClientForModel,
16697
16748
  createClient: () => createClient,
16698
16749
  callOpenAICompatible: () => callOpenAICompatible,
16750
+ buildScenarioUserMessage: () => buildScenarioUserMessage,
16699
16751
  BROWSER_TOOLS: () => BROWSER_TOOLS
16700
16752
  });
16701
16753
  import Anthropic2 from "@anthropic-ai/sdk";
@@ -17108,7 +17160,6 @@ async function executeTool(page, screenshotter, toolName, toolInput, context) {
17108
17160
  const assertionType = toolInput.assertion_type;
17109
17161
  const selector = toolInput.selector;
17110
17162
  const expected = toolInput.expected;
17111
- const sessionId = context.sessionId ?? "default";
17112
17163
  switch (assertionType) {
17113
17164
  case "element_exists": {
17114
17165
  if (!selector)
@@ -17173,7 +17224,6 @@ async function executeTool(page, screenshotter, toolName, toolInput, context) {
17173
17224
  case "browser_intercept": {
17174
17225
  const action = toolInput.action;
17175
17226
  const pattern = toolInput.pattern;
17176
- const interceptAction = toolInput.intercept_action;
17177
17227
  const statusCode = toolInput.status_code;
17178
17228
  const body = toolInput.body;
17179
17229
  const sessionId = context.sessionId ?? "default";
@@ -17250,7 +17300,28 @@ ${JSON.stringify(har, null, 2)}` };
17250
17300
  }
17251
17301
  case "browser_a11y": {
17252
17302
  const level = toolInput.level ?? "AA";
17253
- const snapshot = await page.accessibility.snapshot();
17303
+ const snapshot = await page.evaluate(() => {
17304
+ function readRole(el) {
17305
+ return el.getAttribute("role") ?? el.tagName.toLowerCase();
17306
+ }
17307
+ function readName(el) {
17308
+ const labelledBy = el.getAttribute("aria-labelledby");
17309
+ if (labelledBy) {
17310
+ const labelledText = labelledBy.split(/\s+/).map((id) => document.getElementById(id)?.textContent?.trim()).filter(Boolean).join(" ");
17311
+ if (labelledText)
17312
+ return labelledText;
17313
+ }
17314
+ return el.getAttribute("aria-label") ?? el.getAttribute("alt") ?? el.textContent?.trim() ?? "";
17315
+ }
17316
+ function walk(el) {
17317
+ return {
17318
+ role: readRole(el),
17319
+ name: readName(el),
17320
+ children: Array.from(el.children).map((child) => walk(child))
17321
+ };
17322
+ }
17323
+ return document.body ? walk(document.body) : null;
17324
+ });
17254
17325
  if (!snapshot)
17255
17326
  return { result: "Error: could not capture accessibility tree" };
17256
17327
  const issues = [];
@@ -17292,6 +17363,38 @@ ${filtered.join(`
17292
17363
  return { result: `Error executing ${toolName}: ${message}` };
17293
17364
  }
17294
17365
  }
17366
+ function resolveStartUrl(baseUrl, targetPath) {
17367
+ try {
17368
+ return new URL(targetPath, baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`).toString();
17369
+ } catch {
17370
+ return `${baseUrl.replace(/\/+$/, "")}/${targetPath.replace(/^\/+/, "")}`;
17371
+ }
17372
+ }
17373
+ function buildScenarioUserMessage(scenario, baseUrl) {
17374
+ const userParts = [
17375
+ `**Scenario:** ${scenario.name}`,
17376
+ `**Description:** ${scenario.description}`
17377
+ ];
17378
+ if (baseUrl) {
17379
+ const normalizedBaseUrl = baseUrl.replace(/\/+$/, "");
17380
+ userParts.push(`**Base URL:** ${normalizedBaseUrl}`);
17381
+ if (scenario.targetPath) {
17382
+ userParts.push(`**Start URL:** ${resolveStartUrl(normalizedBaseUrl, scenario.targetPath)}`);
17383
+ }
17384
+ 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.");
17385
+ }
17386
+ if (scenario.targetPath) {
17387
+ userParts.push(`**Target Path:** ${scenario.targetPath}`);
17388
+ }
17389
+ if (scenario.steps.length > 0) {
17390
+ userParts.push("**Steps:**");
17391
+ for (let i = 0;i < scenario.steps.length; i++) {
17392
+ userParts.push(`${i + 1}. ${scenario.steps[i]}`);
17393
+ }
17394
+ }
17395
+ return userParts.join(`
17396
+ `);
17397
+ }
17295
17398
  async function runAgentLoop(options) {
17296
17399
  const {
17297
17400
  client,
@@ -17301,6 +17404,7 @@ async function runAgentLoop(options) {
17301
17404
  model,
17302
17405
  runId,
17303
17406
  sessionId,
17407
+ baseUrl,
17304
17408
  maxTurns = 30,
17305
17409
  onStep,
17306
17410
  persona,
@@ -17348,21 +17452,7 @@ Instructions: ${persona.instructions}` : "",
17348
17452
  "- Verify both positive and negative states"
17349
17453
  ].join(`
17350
17454
  `) + personaSection;
17351
- const userParts = [
17352
- `**Scenario:** ${scenario.name}`,
17353
- `**Description:** ${scenario.description}`
17354
- ];
17355
- if (scenario.targetPath) {
17356
- userParts.push(`**Target Path:** ${scenario.targetPath}`);
17357
- }
17358
- if (scenario.steps.length > 0) {
17359
- userParts.push("**Steps:**");
17360
- for (let i = 0;i < scenario.steps.length; i++) {
17361
- userParts.push(`${i + 1}. ${scenario.steps[i]}`);
17362
- }
17363
- }
17364
- const userMessage = userParts.join(`
17365
- `);
17455
+ const userMessage = buildScenarioUserMessage(scenario, baseUrl);
17366
17456
  const screenshots = [];
17367
17457
  let tokensUsed = 0;
17368
17458
  let stepNumber = 0;
@@ -17425,7 +17515,7 @@ Instructions: ${persona.instructions}` : "",
17425
17515
  if (onStep) {
17426
17516
  onStep({ type: "tool_call", toolName: toolBlock.name, toolInput, stepNumber });
17427
17517
  }
17428
- const execResult = await executeTool(page, screenshotter, toolBlock.name, toolInput, { runId, scenarioSlug, stepNumber, sessionId, a11y });
17518
+ const execResult = await executeTool(page, screenshotter, toolBlock.name, toolInput, { runId, scenarioSlug, stepNumber, sessionId: sessionId ?? runId, a11y });
17429
17519
  if (onStep) {
17430
17520
  onStep({ type: "tool_result", toolName: toolBlock.name, toolResult: execResult.result, stepNumber });
17431
17521
  }
@@ -20624,6 +20714,292 @@ var init_failure_pipeline = __esm(() => {
20624
20714
  init_todos_connector();
20625
20715
  });
20626
20716
 
20717
+ // src/lib/a11y-audit.ts
20718
+ async function runA11yAudit(page, options = {}) {
20719
+ const { level = "AA", rules, exclude = [] } = options;
20720
+ await page.addScriptTag({ url: "https://cdnjs.cloudflare.com/ajax/libs/axe-core/4.9.1/axe.min.js" });
20721
+ const config = {
20722
+ runOnly: {
20723
+ type: level === "AAA" ? "standard" : "tag",
20724
+ values: level === "AAA" ? undefined : [level, "best-practice"]
20725
+ }
20726
+ };
20727
+ if (rules && rules.length > 0) {
20728
+ config.rules = Object.fromEntries(rules.map((r) => [r, { enabled: true }]));
20729
+ }
20730
+ if (exclude.length > 0) {
20731
+ config.exclude = exclude;
20732
+ }
20733
+ const result = await page.evaluate(async (auditConfig) => {
20734
+ const axeResult = await window.axe.run(auditConfig);
20735
+ return axeResult;
20736
+ }, config);
20737
+ const violations = (result.violations ?? []).map((v) => ({
20738
+ id: v.id,
20739
+ impact: v.impact,
20740
+ description: v.description,
20741
+ help: v.help,
20742
+ helpUrl: v.helpUrl,
20743
+ nodes: (v.nodes ?? []).map((n) => ({
20744
+ html: n.html,
20745
+ target: n.target,
20746
+ failureSummary: n.failureSummary
20747
+ }))
20748
+ }));
20749
+ const passes = (result.passes ?? []).map((p) => ({
20750
+ id: p.id,
20751
+ description: p.description
20752
+ }));
20753
+ const incomplete = (result.incomplete ?? []).map((i) => ({
20754
+ id: i.id,
20755
+ description: i.description,
20756
+ impact: i.impact
20757
+ }));
20758
+ const criticalCount = violations.filter((v) => v.impact === "critical").length;
20759
+ const seriousCount = violations.filter((v) => v.impact === "serious").length;
20760
+ const moderateCount = violations.filter((v) => v.impact === "moderate").length;
20761
+ const minorCount = violations.filter((v) => v.impact === "minor").length;
20762
+ return {
20763
+ violations,
20764
+ passes,
20765
+ incomplete,
20766
+ url: page.url(),
20767
+ timestamp: new Date().toISOString(),
20768
+ totalViolations: violations.length,
20769
+ criticalCount,
20770
+ seriousCount,
20771
+ moderateCount,
20772
+ minorCount
20773
+ };
20774
+ }
20775
+
20776
+ // src/lib/assertions.ts
20777
+ async function evaluateAssertions(page, assertions, context = {}) {
20778
+ const results = [];
20779
+ for (const assertion of assertions) {
20780
+ try {
20781
+ const result = await evaluateOne(page, assertion, context);
20782
+ results.push(result);
20783
+ } catch (err) {
20784
+ results.push({
20785
+ assertion,
20786
+ passed: false,
20787
+ actual: "",
20788
+ error: err instanceof Error ? err.message : String(err)
20789
+ });
20790
+ }
20791
+ }
20792
+ return results;
20793
+ }
20794
+ async function evaluateOne(page, assertion, context) {
20795
+ switch (assertion.type) {
20796
+ case "visible": {
20797
+ const visible = await page.locator(assertion.selector).isVisible();
20798
+ return {
20799
+ assertion,
20800
+ passed: visible,
20801
+ actual: String(visible)
20802
+ };
20803
+ }
20804
+ case "not_visible": {
20805
+ const visible = await page.locator(assertion.selector).isVisible();
20806
+ return {
20807
+ assertion,
20808
+ passed: !visible,
20809
+ actual: String(visible)
20810
+ };
20811
+ }
20812
+ case "text_contains": {
20813
+ const text = await page.locator(assertion.selector).textContent() ?? "";
20814
+ const expected = String(assertion.expected ?? "");
20815
+ return {
20816
+ assertion,
20817
+ passed: text.includes(expected),
20818
+ actual: text
20819
+ };
20820
+ }
20821
+ case "text_equals": {
20822
+ const text = await page.locator(assertion.selector).textContent() ?? "";
20823
+ const expected = String(assertion.expected ?? "");
20824
+ return {
20825
+ assertion,
20826
+ passed: text.trim() === expected.trim(),
20827
+ actual: text
20828
+ };
20829
+ }
20830
+ case "element_count": {
20831
+ const count = await page.locator(assertion.selector).count();
20832
+ const expected = Number(assertion.expected ?? 0);
20833
+ return {
20834
+ assertion,
20835
+ passed: count === expected,
20836
+ actual: String(count)
20837
+ };
20838
+ }
20839
+ case "no_console_errors": {
20840
+ if (context.consoleErrors !== undefined) {
20841
+ const errors2 = context.consoleErrors.filter(Boolean);
20842
+ return {
20843
+ assertion,
20844
+ passed: errors2.length === 0,
20845
+ actual: errors2.length === 0 ? "No console errors captured" : errors2.slice(0, 3).join(" | ")
20846
+ };
20847
+ }
20848
+ const errorElements = await page.locator('[role="alert"], .error, .error-message, [data-testid="error"]').count();
20849
+ return {
20850
+ assertion,
20851
+ passed: errorElements === 0,
20852
+ actual: `${errorElements} error element(s) found`
20853
+ };
20854
+ }
20855
+ case "no_a11y_violations": {
20856
+ try {
20857
+ const auditResult = await runA11yAudit(page);
20858
+ const hasIssues = auditResult.violations.length > 0;
20859
+ return {
20860
+ assertion,
20861
+ passed: !hasIssues,
20862
+ actual: hasIssues ? `${auditResult.totalViolations} violation(s): ${auditResult.violations.map((v) => v.id).join(", ")}` : "No accessibility violations found"
20863
+ };
20864
+ } catch (err) {
20865
+ return {
20866
+ assertion,
20867
+ passed: false,
20868
+ actual: "",
20869
+ error: err instanceof Error ? err.message : String(err)
20870
+ };
20871
+ }
20872
+ }
20873
+ case "url_contains": {
20874
+ const url = page.url();
20875
+ const expected = String(assertion.expected ?? "");
20876
+ return {
20877
+ assertion,
20878
+ passed: url.includes(expected),
20879
+ actual: url
20880
+ };
20881
+ }
20882
+ case "title_contains": {
20883
+ const title = await page.title();
20884
+ const expected = String(assertion.expected ?? "");
20885
+ return {
20886
+ assertion,
20887
+ passed: title.includes(expected),
20888
+ actual: title
20889
+ };
20890
+ }
20891
+ case "cookie_exists": {
20892
+ const cookieName = assertion.expected;
20893
+ const cookies = await page.context().cookies();
20894
+ const found = cookies.some((c) => c.name === cookieName);
20895
+ return {
20896
+ assertion,
20897
+ passed: found,
20898
+ actual: found ? `Cookie "${cookieName}" exists` : `Cookie "${cookieName}" not found`
20899
+ };
20900
+ }
20901
+ case "cookie_not_exists": {
20902
+ const cookieName = assertion.expected;
20903
+ const cookies = await page.context().cookies();
20904
+ const found = cookies.some((c) => c.name === cookieName);
20905
+ return {
20906
+ assertion,
20907
+ passed: !found,
20908
+ actual: found ? `Cookie "${cookieName}" found (unexpected)` : `Cookie "${cookieName}" does not exist`
20909
+ };
20910
+ }
20911
+ case "cookie_value": {
20912
+ const [cookieName, expectedValue] = assertion.expected.split("=", 2);
20913
+ const cookies = await page.context().cookies();
20914
+ const cookie = cookies.find((c) => c.name === cookieName);
20915
+ const actualValue = cookie?.value ?? "";
20916
+ return {
20917
+ assertion,
20918
+ passed: actualValue === expectedValue,
20919
+ actual: cookie ? `${cookieName}=${actualValue}` : `Cookie "${cookieName}" not found`
20920
+ };
20921
+ }
20922
+ case "local_storage_exists": {
20923
+ const key = assertion.expected;
20924
+ const value = await page.evaluate((k) => localStorage.getItem(k), key);
20925
+ return {
20926
+ assertion,
20927
+ passed: value !== null,
20928
+ actual: value !== null ? `Key "${key}" exists with value "${value}"` : `Key "${key}" not found in localStorage`
20929
+ };
20930
+ }
20931
+ case "local_storage_not_exists": {
20932
+ const key = assertion.expected;
20933
+ const value = await page.evaluate((k) => localStorage.getItem(k), key);
20934
+ return {
20935
+ assertion,
20936
+ passed: value === null,
20937
+ actual: value !== null ? `Key "${key}" exists (unexpected)` : `Key "${key}" does not exist in localStorage`
20938
+ };
20939
+ }
20940
+ case "local_storage_value": {
20941
+ const [lsKey, expectedValue] = assertion.expected.split("=", 2);
20942
+ const value = await page.evaluate((k) => localStorage.getItem(k), lsKey ?? "");
20943
+ return {
20944
+ assertion,
20945
+ passed: value === expectedValue,
20946
+ actual: value !== null ? `${lsKey}=${value}` : `Key "${lsKey}" not found in localStorage`
20947
+ };
20948
+ }
20949
+ case "session_storage_value": {
20950
+ const [ssKey, expectedValue] = assertion.expected.split("=", 2);
20951
+ const value = await page.evaluate((k) => sessionStorage.getItem(k), ssKey ?? "");
20952
+ return {
20953
+ assertion,
20954
+ passed: value === expectedValue,
20955
+ actual: value !== null ? `${ssKey}=${value}` : `Key "${ssKey}" not found in sessionStorage`
20956
+ };
20957
+ }
20958
+ case "session_storage_not_exists": {
20959
+ const key = assertion.expected;
20960
+ const value = await page.evaluate((k) => sessionStorage.getItem(k), key);
20961
+ return {
20962
+ assertion,
20963
+ passed: value === null,
20964
+ actual: value !== null ? `Key "${key}" exists (unexpected)` : `Key "${key}" does not exist in sessionStorage`
20965
+ };
20966
+ }
20967
+ default: {
20968
+ return {
20969
+ assertion,
20970
+ passed: false,
20971
+ actual: "",
20972
+ error: `Unknown assertion type: ${assertion.type}`
20973
+ };
20974
+ }
20975
+ }
20976
+ }
20977
+ function allAssertionsPassed(results) {
20978
+ return results.every((r) => r.passed);
20979
+ }
20980
+ function formatAssertionResults(results) {
20981
+ if (results.length === 0)
20982
+ return "No assertions.";
20983
+ const lines = [];
20984
+ for (const r of results) {
20985
+ const icon = r.passed ? "PASS" : "FAIL";
20986
+ const desc = r.assertion.description || `${r.assertion.type}${r.assertion.selector ? ` ${r.assertion.selector}` : ""}`;
20987
+ let line = ` [${icon}] ${desc}`;
20988
+ if (!r.passed) {
20989
+ line += ` (actual: ${r.actual})`;
20990
+ if (r.error)
20991
+ line += ` \u2014 ${r.error}`;
20992
+ }
20993
+ lines.push(line);
20994
+ }
20995
+ const passed = results.filter((r) => r.passed).length;
20996
+ lines.push(`
20997
+ ${passed}/${results.length} assertions passed.`);
20998
+ return lines.join(`
20999
+ `);
21000
+ }
21001
+ var init_assertions = () => {};
21002
+
20627
21003
  // src/db/flows.ts
20628
21004
  var exports_flows = {};
20629
21005
  __export(exports_flows, {
@@ -20782,7 +21158,9 @@ __export(exports_runner, {
20782
21158
  runSingleScenario: () => runSingleScenario,
20783
21159
  runByFilter: () => runByFilter,
20784
21160
  runBatch: () => runBatch,
20785
- onRunEvent: () => onRunEvent
21161
+ resolveScenariosForRun: () => resolveScenariosForRun,
21162
+ onRunEvent: () => onRunEvent,
21163
+ applyStructuredAssertionsToResult: () => applyStructuredAssertionsToResult
20786
21164
  });
20787
21165
  import { mkdirSync as mkdirSync8 } from "fs";
20788
21166
  import { join as join13 } from "path";
@@ -20794,6 +21172,54 @@ function emit(event) {
20794
21172
  if (eventHandler)
20795
21173
  eventHandler(event);
20796
21174
  }
21175
+ function assertionDescription(result) {
21176
+ return result.assertion.description || `${result.assertion.type}${result.assertion.selector ? ` ${result.assertion.selector}` : ""}`;
21177
+ }
21178
+ function summarizeAssertionResult(result) {
21179
+ const description = assertionDescription(result);
21180
+ if (result.passed)
21181
+ return description;
21182
+ const suffix = result.error ? `; ${result.error}` : "";
21183
+ return `${description} (actual: ${result.actual}${suffix})`;
21184
+ }
21185
+ async function applyStructuredAssertionsToResult(input) {
21186
+ const assertions = input.scenario.assertions ?? [];
21187
+ if (assertions.length === 0) {
21188
+ return {
21189
+ status: input.status,
21190
+ reasoning: input.reasoning,
21191
+ assertionsPassed: [],
21192
+ assertionsFailed: [],
21193
+ assertionResults: []
21194
+ };
21195
+ }
21196
+ const results = await evaluateAssertions(input.page, assertions, {
21197
+ consoleErrors: input.consoleErrors
21198
+ });
21199
+ const assertionsPassed = results.filter((r) => r.passed).map(summarizeAssertionResult);
21200
+ const assertionsFailed = results.filter((r) => !r.passed).map(summarizeAssertionResult);
21201
+ const assertionResults = results.map((result) => ({
21202
+ type: result.assertion.type,
21203
+ description: assertionDescription(result),
21204
+ passed: result.passed,
21205
+ actual: result.actual,
21206
+ ...result.error ? { error: result.error } : {}
21207
+ }));
21208
+ const assertionsOk = allAssertionsPassed(results);
21209
+ const status = assertionsOk || input.status !== "passed" ? input.status : "failed";
21210
+ const assertionHeading = assertionsOk ? "Structured assertions passed:" : "Structured assertions failed:";
21211
+ const reasoningParts = [input.reasoning, `${assertionHeading}
21212
+ ${formatAssertionResults(results)}`].map((part) => part.trim()).filter(Boolean);
21213
+ return {
21214
+ status,
21215
+ reasoning: reasoningParts.join(`
21216
+
21217
+ `),
21218
+ assertionsPassed,
21219
+ assertionsFailed,
21220
+ assertionResults
21221
+ };
21222
+ }
20797
21223
  function withTimeout(promise, ms, label) {
20798
21224
  return new Promise((resolve, reject) => {
20799
21225
  const warningAt = Math.floor(ms * 0.8);
@@ -20964,6 +21390,7 @@ async function runSingleScenario(scenario, runId, options) {
20964
21390
  model,
20965
21391
  runId,
20966
21392
  sessionId: result.id,
21393
+ baseUrl: options.url,
20967
21394
  maxTurns: effectiveOptions.minimal ? 10 : 30,
20968
21395
  a11y: effectiveOptions.a11y,
20969
21396
  persona: persona ? {
@@ -21046,27 +21473,46 @@ async function runSingleScenario(scenario, runId, options) {
21046
21473
  closeSession(result.id);
21047
21474
  const lightpandaNote = options.engine === "lightpanda" ? " (Running with Lightpanda \u2014 no screenshots)" : options.engine === "bun" ? " (Running with Bun.WebView \u2014 native, ~11x faster)" : "";
21048
21475
  const networkMeta = networkErrors.length > 0 ? { networkErrors: networkErrors.slice(0, 20) } : {};
21049
- let updatedResult = updateResult(result.id, {
21476
+ const baseReasoning = agentResult.reasoning ? agentResult.reasoning + lightpandaNote : lightpandaNote || "";
21477
+ const assertionOutcome = await applyStructuredAssertionsToResult({
21478
+ page,
21479
+ scenario,
21480
+ consoleErrors,
21050
21481
  status: agentResult.status,
21051
- reasoning: agentResult.reasoning ? agentResult.reasoning + lightpandaNote : lightpandaNote || undefined,
21482
+ reasoning: baseReasoning
21483
+ });
21484
+ const structuredAssertionMeta = assertionOutcome.assertionResults.length > 0 ? {
21485
+ structuredAssertions: {
21486
+ passed: assertionOutcome.assertionsPassed,
21487
+ failed: assertionOutcome.assertionsFailed,
21488
+ results: assertionOutcome.assertionResults
21489
+ }
21490
+ } : {};
21491
+ let updatedResult = updateResult(result.id, {
21492
+ status: assertionOutcome.status,
21493
+ reasoning: assertionOutcome.reasoning || undefined,
21052
21494
  stepsCompleted: agentResult.stepsCompleted,
21053
21495
  durationMs: Date.now() - new Date(result.createdAt).getTime(),
21054
21496
  tokensUsed: agentResult.tokensUsed,
21055
21497
  costCents: estimateCost(model, agentResult.tokensUsed),
21056
- metadata: { consoleLogs, ...networkErrors.length > 0 ? networkMeta : {} }
21498
+ metadata: {
21499
+ consoleLogs,
21500
+ ...networkErrors.length > 0 ? networkMeta : {},
21501
+ ...structuredAssertionMeta
21502
+ }
21057
21503
  });
21058
- if (agentResult.status === "failed" || agentResult.status === "error") {
21059
- const failureAnalysis = analyzeFailure(null, agentResult.reasoning ?? null);
21504
+ if (assertionOutcome.status === "failed" || assertionOutcome.status === "error") {
21505
+ const failureAnalysis = analyzeFailure(null, assertionOutcome.reasoning ?? null);
21060
21506
  if (failureAnalysis) {
21061
21507
  updatedResult = updateResult(result.id, { failureAnalysis });
21062
21508
  }
21063
21509
  }
21064
- if (agentResult.status === "passed") {
21510
+ if (assertionOutcome.status === "passed") {
21065
21511
  try {
21066
21512
  updateScenarioPassedCache(scenario.id, options.url);
21067
21513
  } catch {}
21068
21514
  }
21069
- const eventType = agentResult.status === "passed" ? "scenario:pass" : "scenario:fail";
21515
+ const eventType = assertionOutcome.status === "passed" ? "scenario:pass" : "scenario:fail";
21070
21516
  emit({ type: eventType, scenarioId: scenario.id, scenarioName: scenario.name, resultId: result.id, runId });
21071
21517
  return updatedResult;
21072
21518
  } catch (error) {
@@ -21091,7 +21537,8 @@ async function runSingleScenario(scenario, runId, options) {
21091
21537
  } finally {
21092
21538
  if (harPath) {
21093
21539
  try {
21094
- updateResult(result.id, { metadata: { harPath } });
21540
+ const existing = getResult(result.id);
21541
+ updateResult(result.id, { metadata: { ...existing?.metadata ?? {}, harPath } });
21095
21542
  } catch {}
21096
21543
  }
21097
21544
  if (browser) {
@@ -21263,22 +21710,31 @@ async function runBatch(scenarios, options) {
21263
21710
  }
21264
21711
  return { run: finalRun, results };
21265
21712
  }
21266
- async function runByFilter(options) {
21267
- let scenarios;
21713
+ function findScenarioInList(scenarios, id) {
21714
+ return scenarios.find((scenario) => scenario.id === id || scenario.shortId === id || scenario.id.startsWith(id)) ?? null;
21715
+ }
21716
+ function resolveScenariosForRun(options) {
21268
21717
  if (options.scenarioIds && options.scenarioIds.length > 0) {
21269
- const all = listScenarios({ projectId: options.projectId });
21270
- scenarios = all.filter((s) => options.scenarioIds.includes(s.id) || options.scenarioIds.includes(s.shortId));
21271
- if (scenarios.length === 0 && options.projectId) {
21272
- const global2 = listScenarios({});
21273
- scenarios = global2.filter((s) => options.scenarioIds.includes(s.id) || options.scenarioIds.includes(s.shortId));
21718
+ const scoped = listScenarios({ projectId: options.projectId });
21719
+ const resolved = [];
21720
+ const seen = new Set;
21721
+ for (const id of options.scenarioIds) {
21722
+ const scenario = findScenarioInList(scoped, id) ?? getScenario(id);
21723
+ if (scenario && !seen.has(scenario.id)) {
21724
+ resolved.push(scenario);
21725
+ seen.add(scenario.id);
21726
+ }
21274
21727
  }
21275
- } else {
21276
- scenarios = listScenarios({
21277
- projectId: options.projectId,
21278
- tags: options.tags,
21279
- priority: options.priority
21280
- });
21728
+ return resolved;
21281
21729
  }
21730
+ return listScenarios({
21731
+ projectId: options.projectId,
21732
+ tags: options.tags,
21733
+ priority: options.priority
21734
+ });
21735
+ }
21736
+ async function runByFilter(options) {
21737
+ const scenarios = resolveScenariosForRun(options);
21282
21738
  if (scenarios.length === 0) {
21283
21739
  const config = loadConfig();
21284
21740
  const model = resolveModel2(options.model ?? config.defaultModel);
@@ -21291,17 +21747,7 @@ async function runByFilter(options) {
21291
21747
  function startRunAsync(options) {
21292
21748
  const config = loadConfig();
21293
21749
  const model = resolveModel2(options.model ?? config.defaultModel);
21294
- let scenarios;
21295
- if (options.scenarioIds && options.scenarioIds.length > 0) {
21296
- const all = listScenarios({ projectId: options.projectId });
21297
- scenarios = all.filter((s) => options.scenarioIds.includes(s.id) || options.scenarioIds.includes(s.shortId));
21298
- } else {
21299
- scenarios = listScenarios({
21300
- projectId: options.projectId,
21301
- tags: options.tags,
21302
- priority: options.priority
21303
- });
21304
- }
21750
+ const scenarios = resolveScenariosForRun(options);
21305
21751
  if (!options.skipBudgetCheck) {
21306
21752
  const cap = options.maxCostCents ?? config.defaultMaxCostCents;
21307
21753
  if (cap !== undefined && cap > 0 && scenarios.length > 0) {
@@ -21405,6 +21851,7 @@ var init_runner = __esm(() => {
21405
21851
  init_session_tracker();
21406
21852
  init_webhooks();
21407
21853
  init_failure_pipeline();
21854
+ init_assertions();
21408
21855
  });
21409
21856
 
21410
21857
  // src/lib/affected.ts
@@ -22879,18 +23326,7 @@ function normalizeFilter(input) {
22879
23326
  };
22880
23327
  }
22881
23328
  function normalizeExecution(input) {
22882
- const target = input?.target ?? "local";
22883
- if (target === "connector:e2b") {
22884
- return {
22885
- target,
22886
- connector: input?.connector ?? "e2b",
22887
- operation: input?.operation ?? "run",
22888
- sandboxTemplate: input?.sandboxTemplate,
22889
- timeoutMs: input?.timeoutMs,
22890
- env: input?.env
22891
- };
22892
- }
22893
- return { ...DEFAULT_EXECUTION, timeoutMs: input?.timeoutMs };
23329
+ return input ? workflowExecutionFromValue(input) : DEFAULT_EXECUTION;
22894
23330
  }
22895
23331
  function createTestingWorkflow(input) {
22896
23332
  const db2 = getDatabase();
@@ -22941,6 +23377,9 @@ var init_workflows = __esm(() => {
22941
23377
  });
22942
23378
 
22943
23379
  // src/lib/workflow-runner.ts
23380
+ import { mkdtempSync, rmSync, writeFileSync as writeFileSync3 } from "fs";
23381
+ import { tmpdir } from "os";
23382
+ import { join as join14 } from "path";
22944
23383
  function buildWorkflowRunPlan(workflow, options) {
22945
23384
  const runOptions = {
22946
23385
  url: options.url,
@@ -22957,10 +23396,10 @@ function buildWorkflowRunPlan(workflow, options) {
22957
23396
  return {
22958
23397
  workflow,
22959
23398
  runOptions,
22960
- connectorCommand: workflow.execution.target === "connector:e2b" ? buildConnectorCommand(workflow.execution, runOptions) : null
23399
+ sandbox: workflow.execution.target === "sandbox" ? buildSandboxPlan(workflow, workflow.execution, runOptions) : null
22961
23400
  };
22962
23401
  }
22963
- async function runTestingWorkflow(workflowId, options) {
23402
+ async function runTestingWorkflow(workflowId, options, dependencies = {}) {
22964
23403
  const workflow = getTestingWorkflow(workflowId);
22965
23404
  if (!workflow)
22966
23405
  throw new Error(`Testing workflow not found: ${workflowId}`);
@@ -22970,13 +23409,25 @@ async function runTestingWorkflow(workflowId, options) {
22970
23409
  const plan = buildWorkflowRunPlan(workflow, options);
22971
23410
  if (options.dryRun)
22972
23411
  return { run: null, results: [], plan };
22973
- if (workflow.execution.target === "connector:e2b") {
22974
- const connectorResult = await runViaConnector(plan);
22975
- return { run: null, results: [], plan, connectorResult };
23412
+ if (workflow.execution.target === "sandbox") {
23413
+ const sandboxResult = await runViaSandbox(plan, dependencies);
23414
+ return { run: null, results: [], plan, sandboxResult };
22976
23415
  }
22977
- const { run, results } = await runByFilter(plan.runOptions);
23416
+ const runLocal = dependencies.runByFilter ?? runByFilter;
23417
+ const { run, results } = await runLocal(plan.runOptions);
22978
23418
  return { run, results, plan };
22979
23419
  }
23420
+ function createWorkflowDatabaseBundle(workflow, plan) {
23421
+ if (!plan.sandbox)
23422
+ throw new Error(`Workflow is not configured for sandbox execution: ${workflow.name}`);
23423
+ const localDir = mkdtempSync(join14(tmpdir(), `testers-workflow-${workflow.id.slice(0, 8)}-`));
23424
+ writeFileSync3(join14(localDir, "testers.db"), getDatabase().serialize());
23425
+ return {
23426
+ localDir,
23427
+ remoteDir: plan.sandbox.stateRemoteDir,
23428
+ cleanup: () => rmSync(localDir, { recursive: true, force: true })
23429
+ };
23430
+ }
22980
23431
  function validatePersonaIds(workflow) {
22981
23432
  for (const personaId of workflow.personaIds) {
22982
23433
  if (!getPersona(personaId)) {
@@ -22984,48 +23435,112 @@ function validatePersonaIds(workflow) {
22984
23435
  }
22985
23436
  }
22986
23437
  }
22987
- function buildConnectorCommand(execution, runOptions) {
22988
- const connector = execution.connector ?? "e2b";
22989
- const operation = execution.operation ?? "run";
22990
- const payload = JSON.stringify({
22991
- operation,
22992
- template: execution.sandboxTemplate,
23438
+ function buildSandboxPlan(workflow, execution, runOptions) {
23439
+ const remoteDir = execution.sandboxRemoteDir ?? `/tmp/testers-workflow-${workflow.id.slice(0, 8)}`;
23440
+ const stateRemoteDir = `${remoteDir.replace(/\/+$/, "")}/.testers-state`;
23441
+ return {
23442
+ provider: execution.provider,
23443
+ image: execution.sandboxImage,
23444
+ name: `testers-${workflow.id.slice(0, 8)}`,
23445
+ remoteDir,
23446
+ stateRemoteDir,
23447
+ cleanup: execution.sandboxCleanup ?? "delete",
22993
23448
  timeoutMs: execution.timeoutMs,
22994
- env: execution.env ?? {},
22995
- command: [
22996
- "bunx",
22997
- "@hasna/testers",
22998
- "run",
22999
- runOptions.url,
23000
- ...runOptions.scenarioIds?.length ? ["--scenario", runOptions.scenarioIds.join(",")] : [],
23001
- ...runOptions.tags?.length ? runOptions.tags.flatMap((tag) => ["--tag", tag]) : [],
23002
- ...runOptions.priority ? ["--priority", runOptions.priority] : [],
23003
- ...runOptions.projectId ? ["--project", runOptions.projectId] : [],
23004
- ...runOptions.model ? ["--model", runOptions.model] : [],
23005
- "--json"
23006
- ]
23007
- });
23008
- return ["connectors", "run", connector, operation, payload];
23449
+ env: execution.env,
23450
+ command: buildSandboxCommand({
23451
+ runOptions,
23452
+ remoteDir,
23453
+ dbPath: `${stateRemoteDir}/testers.db`,
23454
+ setupCommand: execution.setupCommand,
23455
+ packageSpec: execution.packageSpec ?? "@hasna/testers"
23456
+ })
23457
+ };
23009
23458
  }
23010
- async function runViaConnector(plan) {
23011
- if (!plan.connectorCommand)
23012
- throw new Error("Workflow does not have a connector command");
23013
- const proc = Bun.spawn(plan.connectorCommand, {
23014
- stdout: "pipe",
23015
- stderr: "pipe",
23016
- env: process.env
23017
- });
23018
- const [stdout, stderr, exitCode] = await Promise.all([
23019
- new Response(proc.stdout).text(),
23020
- new Response(proc.stderr).text(),
23021
- proc.exited
23022
- ]);
23023
- if (exitCode !== 0) {
23024
- throw new Error(`Connector execution failed (${exitCode}): ${stderr || stdout}`);
23459
+ function buildSandboxCommand(input) {
23460
+ const args = [
23461
+ "bunx",
23462
+ input.packageSpec,
23463
+ "run",
23464
+ input.runOptions.url,
23465
+ ...input.runOptions.scenarioIds?.length ? ["--scenario", input.runOptions.scenarioIds.join(",")] : [],
23466
+ ...input.runOptions.tags?.length ? input.runOptions.tags.flatMap((tag) => ["--tag", tag]) : [],
23467
+ ...input.runOptions.priority ? ["--priority", input.runOptions.priority] : [],
23468
+ ...input.runOptions.projectId ? ["--project", input.runOptions.projectId] : [],
23469
+ ...input.runOptions.model ? ["--model", input.runOptions.model] : [],
23470
+ ...input.runOptions.headed ? ["--headed"] : [],
23471
+ ...input.runOptions.parallel ? ["--parallel", String(input.runOptions.parallel)] : [],
23472
+ ...input.runOptions.timeout ? ["--timeout", String(input.runOptions.timeout)] : [],
23473
+ ...input.runOptions.personaIds?.length ? ["--persona", input.runOptions.personaIds.join(",")] : [],
23474
+ "--no-auto-generate",
23475
+ "--json"
23476
+ ];
23477
+ return [
23478
+ "set -euo pipefail",
23479
+ `mkdir -p ${shellQuote(input.remoteDir)}`,
23480
+ `cd ${shellQuote(input.remoteDir)}`,
23481
+ input.setupCommand,
23482
+ `HASNA_TESTERS_DB_PATH=${shellQuote(input.dbPath)} ${args.map(shellQuote).join(" ")}`
23483
+ ].filter(Boolean).join(`
23484
+ `);
23485
+ }
23486
+ async function runViaSandbox(plan, dependencies) {
23487
+ if (!plan.sandbox)
23488
+ throw new Error("Workflow does not have a sandbox plan");
23489
+ const sandboxes = await resolveSandboxesRuntime(dependencies);
23490
+ const createBundle = dependencies.createDatabaseBundle ?? createWorkflowDatabaseBundle;
23491
+ const bundle = createBundle(plan.workflow, plan);
23492
+ try {
23493
+ const raw = await sandboxes.runCommandInSandbox({
23494
+ command: plan.sandbox.command,
23495
+ provider: plan.sandbox.provider,
23496
+ name: plan.sandbox.name,
23497
+ image: plan.sandbox.image,
23498
+ sandboxTimeout: plan.sandbox.timeoutMs,
23499
+ commandTimeoutMs: plan.sandbox.timeoutMs,
23500
+ projectId: plan.workflow.projectId ?? undefined,
23501
+ config: {
23502
+ source: "testers",
23503
+ workflowId: plan.workflow.id,
23504
+ workflowName: plan.workflow.name
23505
+ },
23506
+ sandboxEnvVars: plan.sandbox.env,
23507
+ cleanup: plan.sandbox.cleanup,
23508
+ upload: {
23509
+ localDir: bundle.localDir,
23510
+ remoteDir: bundle.remoteDir
23511
+ }
23512
+ });
23513
+ const exitCode = raw.result.exit_code ?? raw.result.exitCode ?? 0;
23514
+ const stdout = raw.result.stdout ?? "";
23515
+ const stderr = raw.result.stderr ?? "";
23516
+ if (exitCode !== 0) {
23517
+ throw new Error(`Sandbox workflow execution failed (${exitCode}): ${stderr || stdout}`);
23518
+ }
23519
+ return {
23520
+ sandboxId: raw.sandbox.id,
23521
+ sessionId: raw.session.id,
23522
+ exitCode,
23523
+ stdout,
23524
+ stderr,
23525
+ cleanup: raw.cleanup
23526
+ };
23527
+ } finally {
23528
+ bundle.cleanup?.();
23025
23529
  }
23026
- return stdout.trim();
23530
+ }
23531
+ async function resolveSandboxesRuntime(dependencies) {
23532
+ if (dependencies.sandboxes)
23533
+ return dependencies.sandboxes;
23534
+ if (dependencies.createSandboxesSDK)
23535
+ return dependencies.createSandboxesSDK();
23536
+ const mod = await import("@hasna/sandboxes");
23537
+ return mod.createSandboxesSDK();
23538
+ }
23539
+ function shellQuote(value) {
23540
+ return `'${value.replaceAll("'", `'"'"'`)}'`;
23027
23541
  }
23028
23542
  var init_workflow_runner = __esm(() => {
23543
+ init_database();
23029
23544
  init_workflows();
23030
23545
  init_personas();
23031
23546
  init_runner();
@@ -53049,11 +53564,11 @@ import { exec } from "child_process";
53049
53564
  import { promisify } from "util";
53050
53565
  import { readFileSync as readFileSync3 } from "fs";
53051
53566
  import { webcrypto as crypto2 } from "crypto";
53052
- import { existsSync as existsSync42, writeFileSync as writeFileSync3, readFileSync as readFileSync22, mkdirSync as mkdirSync32 } from "fs";
53567
+ import { existsSync as existsSync42, writeFileSync as writeFileSync32, readFileSync as readFileSync22, mkdirSync as mkdirSync32 } from "fs";
53053
53568
  import { join as join42 } from "path";
53054
53569
  import { Database as Database4 } from "bun:sqlite";
53055
53570
  import { existsSync as existsSync11, mkdirSync as mkdirSync9 } from "fs";
53056
- import { dirname as dirname4, join as join14, resolve as resolve2 } from "path";
53571
+ import { dirname as dirname4, join as join15, resolve as resolve2 } from "path";
53057
53572
  import { existsSync as existsSync22, writeFileSync as writeFileSync4 } from "fs";
53058
53573
  import { join as join22 } from "path";
53059
53574
  import { execSync as execSync2, execFileSync } from "child_process";
@@ -53228,7 +53743,7 @@ function getDbPath2() {
53228
53743
  return process.env["PROJECTS_DB_PATH"];
53229
53744
  }
53230
53745
  const home = process.env["HOME"] || process.env["USERPROFILE"] || "~";
53231
- return join14(home, ".hasna", "projects", "projects.db");
53746
+ return join15(home, ".hasna", "projects", "projects.db");
53232
53747
  }
53233
53748
  function ensureDir2(filePath) {
53234
53749
  if (filePath === ":memory:")
@@ -53516,7 +54031,7 @@ function setIntegrations(id, integrations, db2) {
53516
54031
  const jsonPath = join42(project.path, ".project.json");
53517
54032
  if (existsSync42(jsonPath)) {
53518
54033
  const existing = JSON.parse(readFileSync22(jsonPath, "utf-8"));
53519
- writeFileSync3(jsonPath, JSON.stringify({ ...existing, integrations: merged }, null, 2) + `
54034
+ writeFileSync32(jsonPath, JSON.stringify({ ...existing, integrations: merged }, null, 2) + `
53520
54035
  `, "utf-8");
53521
54036
  }
53522
54037
  } catch {}
@@ -69369,11 +69884,11 @@ More information can be found at: https://a.co/c895JFp`);
69369
69884
  var numberSelector = (obj, key, type2) => {
69370
69885
  if (!(key in obj))
69371
69886
  return;
69372
- const numberValue = parseInt(obj[key], 10);
69373
- if (Number.isNaN(numberValue)) {
69887
+ const numberValue2 = parseInt(obj[key], 10);
69888
+ if (Number.isNaN(numberValue2)) {
69374
69889
  throw new TypeError(`Cannot load ${type2} '${key}'. Expected number, got '${obj[key]}'.`);
69375
69890
  }
69376
- return numberValue;
69891
+ return numberValue2;
69377
69892
  };
69378
69893
  exports.SelectorType = undefined;
69379
69894
  (function(SelectorType2) {
@@ -84853,10 +85368,10 @@ __export(exports_contacts_connector, {
84853
85368
  function getContactsDb() {
84854
85369
  const { Database: Database5 } = __require("bun:sqlite");
84855
85370
  const { existsSync: existsSync5 } = __require("fs");
84856
- const { join: join15 } = __require("path");
85371
+ const { join: join16 } = __require("path");
84857
85372
  const { homedir: homedir7 } = __require("os");
84858
85373
  const envPath = process.env["HASNA_CONTACTS_DB_PATH"] ?? process.env["OPEN_CONTACTS_DB"];
84859
- const dbPath = envPath ?? join15(homedir7(), ".hasna", "contacts", "contacts.db");
85374
+ const dbPath = envPath ?? join16(homedir7(), ".hasna", "contacts", "contacts.db");
84860
85375
  if (!existsSync5(dbPath))
84861
85376
  return null;
84862
85377
  const db2 = new Database5(dbPath, { readonly: true });
@@ -84977,7 +85492,7 @@ __export(exports_army_runner, {
84977
85492
  waitForArmyRun: () => waitForArmyRun,
84978
85493
  runWithArmy: () => runWithArmy
84979
85494
  });
84980
- import { join as join15 } from "path";
85495
+ import { join as join16 } from "path";
84981
85496
  function chunkArray(arr, n2) {
84982
85497
  const chunks = [];
84983
85498
  const size = Math.ceil(arr.length / n2);
@@ -84987,7 +85502,7 @@ function chunkArray(arr, n2) {
84987
85502
  return chunks;
84988
85503
  }
84989
85504
  function getCliPath() {
84990
- const srcPath = join15(import.meta.dir, "../cli/index.tsx");
85505
+ const srcPath = join16(import.meta.dir, "../cli/index.tsx");
84991
85506
  return srcPath;
84992
85507
  }
84993
85508
  async function runWithArmy(options) {
@@ -85759,9 +86274,30 @@ function buildServer() {
85759
86274
  goalPrompt: exports_external.string().optional().describe("Goal prompt for the AI SDK workflow agent"),
85760
86275
  successCriteria: exports_external.array(exports_external.string()).optional().describe("Goal success criteria"),
85761
86276
  maxIterations: exports_external.number().int().min(1).max(20).optional().describe("Max goal loop iterations"),
85762
- executionTarget: exports_external.enum(["local", "connector:e2b"]).optional().describe("Run locally or through the open-connectors E2B connector"),
85763
- e2bTemplate: exports_external.string().optional().describe("E2B sandbox template for connector:e2b")
85764
- }, async ({ name: name21, description, projectId, scenarioIds, tags, priority, personaIds, goalPrompt, successCriteria, maxIterations, executionTarget, e2bTemplate }) => {
86277
+ executionTarget: exports_external.enum(["local", "sandbox", "connector:e2b"]).optional().describe("Run locally or through the sandboxes SDK"),
86278
+ sandboxProvider: exports_external.string().optional().describe("Sandbox provider: e2b, daytona, or modal"),
86279
+ sandboxImage: exports_external.string().optional().describe("Sandbox image/template"),
86280
+ sandboxRemoteDir: exports_external.string().optional().describe("Remote working directory for sandbox runs"),
86281
+ sandboxCleanup: exports_external.enum(["delete", "stop", "keep"]).optional().describe("Sandbox cleanup mode"),
86282
+ e2bTemplate: exports_external.string().optional().describe("Legacy alias for sandboxImage")
86283
+ }, async ({
86284
+ name: name21,
86285
+ description,
86286
+ projectId,
86287
+ scenarioIds,
86288
+ tags,
86289
+ priority,
86290
+ personaIds,
86291
+ goalPrompt,
86292
+ successCriteria,
86293
+ maxIterations,
86294
+ executionTarget,
86295
+ sandboxProvider,
86296
+ sandboxImage,
86297
+ sandboxRemoteDir,
86298
+ sandboxCleanup,
86299
+ e2bTemplate
86300
+ }) => {
85765
86301
  try {
85766
86302
  return json3(createTestingWorkflow({
85767
86303
  name: name21,
@@ -85772,8 +86308,10 @@ function buildServer() {
85772
86308
  goal: goalPrompt ? { prompt: goalPrompt, successCriteria, maxIterations } : null,
85773
86309
  execution: {
85774
86310
  target: executionTarget ?? "local",
85775
- connector: executionTarget === "connector:e2b" ? "e2b" : undefined,
85776
- sandboxTemplate: e2bTemplate
86311
+ provider: sandboxProvider ?? (executionTarget === "connector:e2b" ? "e2b" : undefined),
86312
+ sandboxImage: sandboxImage ?? e2bTemplate,
86313
+ sandboxRemoteDir,
86314
+ sandboxCleanup
85777
86315
  }
85778
86316
  }));
85779
86317
  } catch (error40) {