@hasna/testers 0.0.27 → 0.0.29

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.
Files changed (77) hide show
  1. package/LICENSE +1 -154
  2. package/README.md +60 -0
  3. package/dist/cli/index.js +954 -78
  4. package/dist/db/database.d.ts.map +1 -1
  5. package/dist/db/personas.d.ts.map +1 -1
  6. package/dist/db/runs.d.ts +29 -0
  7. package/dist/db/runs.d.ts.map +1 -1
  8. package/dist/db/scenarios.d.ts +12 -0
  9. package/dist/db/scenarios.d.ts.map +1 -1
  10. package/dist/db/sessions.d.ts +36 -0
  11. package/dist/db/sessions.d.ts.map +1 -0
  12. package/dist/db/step-results.d.ts +30 -0
  13. package/dist/db/step-results.d.ts.map +1 -0
  14. package/dist/index.d.ts +1 -0
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +439 -24
  17. package/dist/lib/a11y-audit.d.ts +54 -0
  18. package/dist/lib/a11y-audit.d.ts.map +1 -0
  19. package/dist/lib/api-discovery.d.ts +46 -0
  20. package/dist/lib/api-discovery.d.ts.map +1 -0
  21. package/dist/lib/assertions.d.ts.map +1 -1
  22. package/dist/lib/auth-profiles.d.ts +16 -0
  23. package/dist/lib/auth-profiles.d.ts.map +1 -0
  24. package/dist/lib/auth-session-pool.d.ts +57 -0
  25. package/dist/lib/auth-session-pool.d.ts.map +1 -0
  26. package/dist/lib/batch-actions.d.ts +44 -0
  27. package/dist/lib/batch-actions.d.ts.map +1 -0
  28. package/dist/lib/browser-compat.d.ts +14 -0
  29. package/dist/lib/browser-compat.d.ts.map +1 -0
  30. package/dist/lib/browser.d.ts +7 -8
  31. package/dist/lib/browser.d.ts.map +1 -1
  32. package/dist/lib/ci.d.ts +12 -0
  33. package/dist/lib/ci.d.ts.map +1 -1
  34. package/dist/lib/discovery.d.ts +23 -0
  35. package/dist/lib/discovery.d.ts.map +1 -0
  36. package/dist/lib/dom-mutation.d.ts +53 -0
  37. package/dist/lib/dom-mutation.d.ts.map +1 -0
  38. package/dist/lib/environment.d.ts +26 -0
  39. package/dist/lib/environment.d.ts.map +1 -0
  40. package/dist/lib/health-scan.d.ts +2 -1
  41. package/dist/lib/health-scan.d.ts.map +1 -1
  42. package/dist/lib/junit-export.d.ts +24 -0
  43. package/dist/lib/junit-export.d.ts.map +1 -0
  44. package/dist/lib/network-mock.d.ts +38 -0
  45. package/dist/lib/network-mock.d.ts.map +1 -0
  46. package/dist/lib/offline-mode.d.ts +31 -0
  47. package/dist/lib/offline-mode.d.ts.map +1 -0
  48. package/dist/lib/pdf-export.d.ts +27 -0
  49. package/dist/lib/pdf-export.d.ts.map +1 -0
  50. package/dist/lib/performance.d.ts +65 -0
  51. package/dist/lib/performance.d.ts.map +1 -0
  52. package/dist/lib/pr-comment.d.ts +27 -0
  53. package/dist/lib/pr-comment.d.ts.map +1 -0
  54. package/dist/lib/preview-detect.d.ts +27 -0
  55. package/dist/lib/preview-detect.d.ts.map +1 -0
  56. package/dist/lib/recorder.d.ts +42 -0
  57. package/dist/lib/recorder.d.ts.map +1 -1
  58. package/dist/lib/repo-discovery.d.ts +102 -0
  59. package/dist/lib/repo-discovery.d.ts.map +1 -0
  60. package/dist/lib/repo-executor.d.ts +56 -0
  61. package/dist/lib/repo-executor.d.ts.map +1 -0
  62. package/dist/lib/responsive.d.ts +43 -0
  63. package/dist/lib/responsive.d.ts.map +1 -0
  64. package/dist/lib/runner.d.ts +1 -0
  65. package/dist/lib/runner.d.ts.map +1 -1
  66. package/dist/lib/scenario-chain.d.ts +52 -0
  67. package/dist/lib/scenario-chain.d.ts.map +1 -0
  68. package/dist/lib/templates.d.ts.map +1 -1
  69. package/dist/lib/webhooks.d.ts +3 -0
  70. package/dist/lib/webhooks.d.ts.map +1 -1
  71. package/dist/mcp/index.js +491 -38
  72. package/dist/sdk/index.d.ts +47 -0
  73. package/dist/sdk/index.d.ts.map +1 -0
  74. package/dist/server/index.js +274 -28
  75. package/dist/types/index.d.ts +64 -2
  76. package/dist/types/index.d.ts.map +1 -1
  77. package/package.json +1 -1
@@ -0,0 +1,47 @@
1
+ /**
2
+ * SDK/Library API for programmatic use of open-testers.
3
+ * Use this as the entry point when integrating open-testers into
4
+ * Node.js/TypeScript projects without going through the MCP server.
5
+ *
6
+ * ```typescript
7
+ * import { createScenario, listScenarioTemplates, RunOptions } from "open-testers/sdk";
8
+ * ```
9
+ */
10
+ export type { Scenario, Run, Result, RunOptions, CreateScenarioInput, UpdateScenarioInput, Persona, CreatePersonaInput, UpdatePersonaInput, PersonaAuth, Webhook, AuthProfile, AuthStrategy, Assertion, AssertionType, ScenarioPriority, ResultStatus, ModelPreset, BrowserEngine, DevicePreset, } from "../types/index.js";
11
+ export type { RunEvent } from "../lib/runner.js";
12
+ export type { MockRule } from "../lib/network-mock.js";
13
+ export type { BatchAction, BatchActionResult } from "../lib/batch-actions.js";
14
+ export type { MutationEvent, MutationOptions } from "../lib/dom-mutation.js";
15
+ export type { WebVitals, PerformanceBudget, BudgetViolation, PerformanceResult } from "../lib/performance.js";
16
+ export type { A11yAuditResult, A11yAuditOptions, A11yViolation } from "../lib/a11y-audit.js";
17
+ export type { Environment, EnvironmentInfo } from "../lib/environment.js";
18
+ export type { ThrottleProfile } from "../lib/offline-mode.js";
19
+ export type { ChainOutput, ChainLink } from "../lib/scenario-chain.js";
20
+ export { createScenario, getScenario, getScenarioByShortId, listScenarios, updateScenario, deleteScenario, findStaleScenarios, } from "../db/scenarios.js";
21
+ export { getRun, listRuns, updateRun, countRuns, } from "../db/runs.js";
22
+ export { createResult, getResult, listResults, getResultsByRun, updateResult, } from "../db/results.js";
23
+ export { createStepResult, getStepResult, listStepResults, updateStepResult, } from "../db/step-results.js";
24
+ export { createPersona, getPersona, listPersonas, updatePersona, deletePersona, listAuthenticatedPersonas, savePersonaAuthCookies, } from "../db/personas.js";
25
+ export { getTemplate, listTemplateNames, SCENARIO_TEMPLATES, } from "../lib/templates.js";
26
+ export { generateHtmlReport, generateLatestReport, imageToBase64, } from "../lib/report.js";
27
+ export { saveHtmlReport, generatePdfReport, } from "../lib/pdf-export.js";
28
+ export { toJUnitXml, } from "../lib/junit-export.js";
29
+ export { DEVICE_PRESETS, setDevicePreset, setViewport, captureResponsiveScreenshots, isMobileViewport, listDevicePresets, } from "../lib/responsive.js";
30
+ export { batchActions, hasBatchFailures, formatBatchResults, } from "../lib/batch-actions.js";
31
+ export { watchMutations, waitForElement, waitForElementRemoved, waitForText, snapshotDOM, compareSnapshots, extractElements, } from "../lib/dom-mutation.js";
32
+ export { collectPerformanceMetrics, collectWebVitals, checkBudget, formatPerformanceResult, DEFAULT_BUDGET, } from "../lib/performance.js";
33
+ export { runA11yAudit, hasA11yIssues, formatA11yResults, } from "../lib/a11y-audit.js";
34
+ export { evaluateAssertions, parseAssertionString, allAssertionsPassed, formatAssertionResults, } from "../lib/assertions.js";
35
+ export type { AssertionResult } from "../lib/assertions.js";
36
+ export { getEnvInfo, detectEnvironment, getEnvironmentOverride, } from "../lib/environment.js";
37
+ export { setupNetworkMocks, MockPresets, } from "../lib/network-mock.js";
38
+ export { goOffline, goOnline, testOfflineHandling, enableThrottling, disableThrottling, THROTTLE_PROFILES, } from "../lib/offline-mode.js";
39
+ export { applyChainOutput, resolveChain, extractChainOutput, hasChainDependency, } from "../lib/scenario-chain.js";
40
+ export { authenticateWithProfile, serializeProfile, deserializeProfile, } from "../lib/auth-profiles.js";
41
+ export { discoverApiEndpoints, generateApiScenarios, groupEndpoints, summarizeEndpoints, } from "../lib/api-discovery.js";
42
+ export { setBaseline, getBaseline, compareImages, compareRunScreenshots, formatVisualDiffTerminal, } from "../lib/visual-diff.js";
43
+ export type { VisualDiffResult } from "../lib/visual-diff.js";
44
+ export { startRunAsync, runSingleScenario, runBatch, runByFilter, onRunEvent, } from "../lib/runner.js";
45
+ export type { RunOptions, RunEvent, RunEventHandler } from "../lib/runner.js";
46
+ export { launchBrowser, getPage, closeBrowser, BrowserPool, launchBrowserEngine, installBrowser, } from "../lib/browser.js";
47
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/sdk/index.ts"],"names":[],"mappings":"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"}
@@ -77,7 +77,8 @@ function scenarioFromRow(row) {
77
77
  createdAt: row.created_at,
78
78
  updatedAt: row.updated_at,
79
79
  lastPassedAt: row.last_passed_at ?? null,
80
- lastPassedUrl: row.last_passed_url ?? null
80
+ lastPassedUrl: row.last_passed_url ?? null,
81
+ parameters: row.parameters ? JSON.parse(row.parameters) : null
81
82
  };
82
83
  }
83
84
  function runFromRow(row) {
@@ -97,7 +98,14 @@ function runFromRow(row) {
97
98
  metadata: row.metadata ? JSON.parse(row.metadata) : null,
98
99
  isBaseline: row.is_baseline === 1,
99
100
  samples: row.samples ?? 1,
100
- flakinessThreshold: row.flakiness_threshold ?? 0.95
101
+ flakinessThreshold: row.flakiness_threshold ?? 0.95,
102
+ prNumber: row.pr_number ?? null,
103
+ prTitle: row.pr_title ?? null,
104
+ prBranch: row.pr_branch ?? null,
105
+ prBaseBranch: row.pr_base_branch ?? null,
106
+ prCommitSha: row.pr_commit_sha ?? null,
107
+ prUrl: row.pr_url ?? null,
108
+ ghAppInstallationId: row.gh_app_installation_id ?? null
101
109
  };
102
110
  }
103
111
  function resultFromRow(row) {
@@ -118,7 +126,8 @@ function resultFromRow(row) {
118
126
  createdAt: row.created_at,
119
127
  personaId: row.persona_id ?? null,
120
128
  personaName: row.persona_name ?? null,
121
- failureAnalysis: row.failure_analysis ? JSON.parse(row.failure_analysis) : null
129
+ failureAnalysis: row.failure_analysis ? JSON.parse(row.failure_analysis) : null,
130
+ harPath: row.har_path ?? null
122
131
  };
123
132
  }
124
133
  function screenshotFromRow(row) {
@@ -210,7 +219,10 @@ function personaFromRow(row) {
210
219
  email: row.auth_email,
211
220
  password: row.auth_password,
212
221
  loginPath: row.auth_login_path ?? "/login",
213
- cookies: row.auth_cookies ? JSON.parse(row.auth_cookies) : null
222
+ cookies: row.auth_cookies ? JSON.parse(row.auth_cookies) : null,
223
+ strategy: row.auth_strategy ?? "form-login",
224
+ headers: row.auth_headers ? JSON.parse(row.auth_headers) : undefined,
225
+ customScript: row.auth_script ?? undefined
214
226
  } : null
215
227
  };
216
228
  }
@@ -10141,6 +10153,43 @@ ALTER TABLE scenarios ADD COLUMN required_role TEXT;
10141
10153
  machine_id TEXT,
10142
10154
  created_at TEXT NOT NULL DEFAULT (datetime('now'))
10143
10155
  );
10156
+ `,
10157
+ `
10158
+ ALTER TABLE results ADD COLUMN har_path TEXT;
10159
+ `,
10160
+ `
10161
+ ALTER TABLE scenarios ADD COLUMN parameters TEXT;
10162
+ `,
10163
+ `
10164
+ ALTER TABLE personas ADD COLUMN auth_strategy TEXT DEFAULT 'form-login';
10165
+ ALTER TABLE personas ADD COLUMN auth_headers TEXT;
10166
+ ALTER TABLE personas ADD COLUMN auth_script TEXT;
10167
+ `,
10168
+ `
10169
+ CREATE TABLE IF NOT EXISTS step_results (
10170
+ id TEXT PRIMARY KEY,
10171
+ result_id TEXT NOT NULL REFERENCES results(id) ON DELETE CASCADE,
10172
+ step_number INTEGER NOT NULL,
10173
+ action TEXT NOT NULL,
10174
+ status TEXT NOT NULL DEFAULT 'running' CHECK(status IN ('passed','failed','error','running','skipped')),
10175
+ tool_name TEXT,
10176
+ tool_input TEXT,
10177
+ tool_result TEXT,
10178
+ thinking TEXT,
10179
+ error TEXT,
10180
+ duration_ms INTEGER,
10181
+ screenshot_id TEXT REFERENCES screenshots(id),
10182
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
10183
+ );
10184
+ `,
10185
+ `
10186
+ ALTER TABLE runs ADD COLUMN pr_number INTEGER;
10187
+ ALTER TABLE runs ADD COLUMN pr_title TEXT;
10188
+ ALTER TABLE runs ADD COLUMN pr_branch TEXT;
10189
+ ALTER TABLE runs ADD COLUMN pr_base_branch TEXT;
10190
+ ALTER TABLE runs ADD COLUMN pr_commit_sha TEXT;
10191
+ ALTER TABLE runs ADD COLUMN pr_url TEXT;
10192
+ ALTER TABLE runs ADD COLUMN gh_app_installation_id TEXT;
10144
10193
  `
10145
10194
  ];
10146
10195
  });
@@ -10862,6 +10911,16 @@ async function launchBrowser(options) {
10862
10911
  const headless = options?.headless ?? true;
10863
10912
  const viewport = options?.viewport ?? DEFAULT_VIEWPORT;
10864
10913
  try {
10914
+ if (engine === "playwright-firefox") {
10915
+ const { firefox } = await import("playwright");
10916
+ const browser = await firefox.launch({ headless });
10917
+ return browser;
10918
+ }
10919
+ if (engine === "playwright-webkit") {
10920
+ const { webkit } = await import("playwright");
10921
+ const browser = await webkit.launch({ headless });
10922
+ return browser;
10923
+ }
10865
10924
  return await launchPlaywright({ headless, viewport });
10866
10925
  } catch (error) {
10867
10926
  const message = error instanceof Error ? error.message : String(error);
@@ -10978,8 +11037,9 @@ async function installBrowser(engine) {
10978
11037
  const { installLightpanda: installLightpanda2 } = await Promise.resolve().then(() => (init_browser_lightpanda(), exports_browser_lightpanda));
10979
11038
  return installLightpanda2();
10980
11039
  }
11040
+ const browserName = engine === "playwright-firefox" ? "firefox" : engine === "playwright-webkit" ? "webkit" : "chromium";
10981
11041
  try {
10982
- execSync("bunx playwright install chromium", {
11042
+ execSync(`bunx playwright install ${browserName}`, {
10983
11043
  stdio: "inherit"
10984
11044
  });
10985
11045
  } catch (error) {
@@ -11111,7 +11171,7 @@ function getDefaultConfig() {
11111
11171
  browser: {
11112
11172
  headless: true,
11113
11173
  viewport: { width: 1280, height: 720 },
11114
- timeout: 60000
11174
+ timeout: 120000
11115
11175
  },
11116
11176
  screenshots: {
11117
11177
  dir: join8(getTestersDir(), "screenshots"),
@@ -12510,7 +12570,7 @@ var init_scan_issues = __esm(() => {
12510
12570
  // src/server/index.ts
12511
12571
  init_paths();
12512
12572
  import { existsSync as existsSync10 } from "fs";
12513
- import { join as join12 } from "path";
12573
+ import { join as join13 } from "path";
12514
12574
 
12515
12575
  // node_modules/zod/v3/external.js
12516
12576
  var exports_external = {};
@@ -16506,9 +16566,9 @@ function createScenario(input) {
16506
16566
  const short_id = nextShortId(input.projectId);
16507
16567
  const timestamp = now();
16508
16568
  db2.query(`
16509
- INSERT INTO scenarios (id, short_id, project_id, name, description, steps, tags, priority, model, timeout_ms, target_path, requires_auth, auth_config, metadata, assertions, version, created_at, updated_at)
16510
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?, ?)
16511
- `).run(id, short_id, input.projectId ?? null, input.name, input.description, JSON.stringify(input.steps ?? []), JSON.stringify(input.tags ?? []), input.priority ?? "medium", input.model ?? null, input.timeoutMs ?? null, input.targetPath ?? null, input.requiresAuth ? 1 : 0, input.authConfig ? JSON.stringify(input.authConfig) : null, input.metadata ? JSON.stringify(input.metadata) : null, JSON.stringify(input.assertions ?? []), timestamp, timestamp);
16569
+ INSERT INTO scenarios (id, short_id, project_id, name, description, steps, tags, priority, model, timeout_ms, target_path, requires_auth, auth_config, metadata, assertions, parameters, version, created_at, updated_at)
16570
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?, ?)
16571
+ `).run(id, short_id, input.projectId ?? null, input.name, input.description, JSON.stringify(input.steps ?? []), JSON.stringify(input.tags ?? []), input.priority ?? "medium", input.model ?? null, input.timeoutMs ?? null, input.targetPath ?? null, input.requiresAuth ? 1 : 0, input.authConfig ? JSON.stringify(input.authConfig) : null, input.metadata ? JSON.stringify(input.metadata) : null, JSON.stringify(input.assertions ?? []), input.parameters ? JSON.stringify(input.parameters) : null, timestamp, timestamp);
16512
16572
  return getScenario(id);
16513
16573
  }
16514
16574
  function getScenario(id) {
@@ -16658,6 +16718,10 @@ function updateScenario(id, input, version) {
16658
16718
  sets.push("assertions = ?");
16659
16719
  params.push(JSON.stringify(input.assertions));
16660
16720
  }
16721
+ if (input.parameters !== undefined) {
16722
+ sets.push("parameters = ?");
16723
+ params.push(JSON.stringify(input.parameters));
16724
+ }
16661
16725
  if (sets.length === 0) {
16662
16726
  return existing;
16663
16727
  }
@@ -16723,9 +16787,9 @@ function createRun(input) {
16723
16787
  const id = uuid();
16724
16788
  const timestamp = now();
16725
16789
  db2.query(`
16726
- INSERT INTO runs (id, project_id, status, url, model, headed, parallel, total, passed, failed, started_at, finished_at, metadata, samples, flakiness_threshold)
16727
- VALUES (?, ?, 'pending', ?, ?, ?, ?, 0, 0, 0, ?, NULL, ?, ?, ?)
16728
- `).run(id, input.projectId ?? null, input.url, input.model, input.headed ? 1 : 0, input.parallel ?? 1, timestamp, input.model ? JSON.stringify({}) : null, input.samples ?? 1, input.flakinessThreshold ?? 0.95);
16790
+ INSERT INTO runs (id, project_id, status, url, model, headed, parallel, total, passed, failed, started_at, finished_at, metadata, samples, flakiness_threshold, pr_number, pr_title, pr_branch, pr_base_branch, pr_commit_sha, pr_url, gh_app_installation_id)
16791
+ VALUES (?, ?, 'pending', ?, ?, ?, ?, 0, 0, 0, ?, NULL, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
16792
+ `).run(id, input.projectId ?? null, input.url, input.model, input.headed ? 1 : 0, input.parallel ?? 1, timestamp, JSON.stringify({}), input.samples ?? 1, input.flakinessThreshold ?? 0.95, input.prNumber ?? null, input.prTitle ?? null, input.prBranch ?? null, input.prBaseBranch ?? null, input.prCommitSha ?? null, input.prUrl ?? null, input.ghAppInstallationId ?? null);
16729
16793
  return getRun(id);
16730
16794
  }
16731
16795
  function getRun(id) {
@@ -16753,6 +16817,14 @@ function listRuns(filter) {
16753
16817
  conditions.push("status = ?");
16754
16818
  params.push(filter.status);
16755
16819
  }
16820
+ if (filter?.since) {
16821
+ conditions.push("started_at >= ?");
16822
+ params.push(filter.since);
16823
+ }
16824
+ if (filter?.until) {
16825
+ conditions.push("started_at <= ?");
16826
+ params.push(filter.until);
16827
+ }
16756
16828
  let sql = "SELECT * FROM runs";
16757
16829
  if (conditions.length > 0) {
16758
16830
  sql += " WHERE " + conditions.join(" AND ");
@@ -16784,6 +16856,14 @@ function countRuns(filter) {
16784
16856
  conditions.push("status = ?");
16785
16857
  params.push(filter.status);
16786
16858
  }
16859
+ if (filter?.since) {
16860
+ conditions.push("started_at >= ?");
16861
+ params.push(filter.since);
16862
+ }
16863
+ if (filter?.until) {
16864
+ conditions.push("started_at <= ?");
16865
+ params.push(filter.until);
16866
+ }
16787
16867
  let sql = "SELECT COUNT(*) as count FROM runs";
16788
16868
  if (conditions.length > 0)
16789
16869
  sql += " WHERE " + conditions.join(" AND ");
@@ -17378,6 +17458,8 @@ async function runPipelineScenario(scenario, options) {
17378
17458
 
17379
17459
  // src/lib/runner.ts
17380
17460
  init_results();
17461
+ import { mkdirSync as mkdirSync6 } from "fs";
17462
+ import { join as join12 } from "path";
17381
17463
 
17382
17464
  // src/lib/failure-analyzer.ts
17383
17465
  function analyzeFailure(error, reasoning) {
@@ -17500,6 +17582,74 @@ function estimateRunCostCents(scenarioCount, model, samples = 1) {
17500
17582
  return scenarioCount * costPerScenario * Math.max(1, samples);
17501
17583
  }
17502
17584
 
17585
+ // src/db/step-results.ts
17586
+ init_database();
17587
+ function createStepResult(input) {
17588
+ const db2 = getDatabase();
17589
+ const id = uuid();
17590
+ const timestamp = now();
17591
+ db2.query(`
17592
+ INSERT INTO step_results (id, result_id, step_number, action, status, tool_name, tool_input, thinking, created_at)
17593
+ VALUES (?, ?, ?, ?, 'running', ?, ?, ?, ?)
17594
+ `).run(id, input.resultId, input.stepNumber, input.action, input.toolName ?? null, input.toolInput ? JSON.stringify(input.toolInput) : null, input.thinking ?? null, timestamp);
17595
+ return getStepResult(id);
17596
+ }
17597
+ function getStepResult(id) {
17598
+ const db2 = getDatabase();
17599
+ const row = db2.query("SELECT * FROM step_results WHERE id = ?").get(id);
17600
+ return row ? stepResultFromRow(row) : null;
17601
+ }
17602
+ function updateStepResult(id, updates) {
17603
+ const db2 = getDatabase();
17604
+ const existing = getStepResult(id);
17605
+ if (!existing)
17606
+ return null;
17607
+ const sets = [];
17608
+ const params = [];
17609
+ if (updates.status !== undefined) {
17610
+ sets.push("status = ?");
17611
+ params.push(updates.status);
17612
+ }
17613
+ if (updates.toolResult !== undefined) {
17614
+ sets.push("tool_result = ?");
17615
+ params.push(updates.toolResult);
17616
+ }
17617
+ if (updates.error !== undefined) {
17618
+ sets.push("error = ?");
17619
+ params.push(updates.error);
17620
+ }
17621
+ if (updates.durationMs !== undefined) {
17622
+ sets.push("duration_ms = ?");
17623
+ params.push(updates.durationMs);
17624
+ }
17625
+ if (updates.screenshotId !== undefined) {
17626
+ sets.push("screenshot_id = ?");
17627
+ params.push(updates.screenshotId);
17628
+ }
17629
+ if (sets.length === 0)
17630
+ return existing;
17631
+ params.push(id);
17632
+ db2.query(`UPDATE step_results SET ${sets.join(", ")} WHERE id = ?`).run(...params);
17633
+ return getStepResult(id);
17634
+ }
17635
+ function stepResultFromRow(row) {
17636
+ return {
17637
+ id: row.id,
17638
+ resultId: row.result_id,
17639
+ stepNumber: row.step_number,
17640
+ action: row.action,
17641
+ status: row.status,
17642
+ toolName: row.tool_name,
17643
+ toolInput: row.tool_input ? JSON.parse(row.tool_input) : null,
17644
+ toolResult: row.tool_result,
17645
+ thinking: row.thinking,
17646
+ error: row.error,
17647
+ durationMs: row.duration_ms,
17648
+ screenshotId: row.screenshot_id,
17649
+ createdAt: row.created_at
17650
+ };
17651
+ }
17652
+
17503
17653
  // src/db/personas.ts
17504
17654
  init_types();
17505
17655
  init_database();
@@ -17509,9 +17659,9 @@ function createPersona(input) {
17509
17659
  const short_id = shortUuid();
17510
17660
  const timestamp = now();
17511
17661
  db2.query(`
17512
- INSERT INTO personas (id, short_id, project_id, name, description, role, instructions, traits, goals, behaviors, expertise_level, demographics, pain_points, metadata, enabled, auth_email, auth_password, auth_login_path, version, created_at, updated_at)
17513
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?, ?)
17514
- `).run(id, short_id, input.projectId ?? null, input.name, input.description ?? "", input.role, input.instructions ?? "", JSON.stringify(input.traits ?? []), JSON.stringify(input.goals ?? []), JSON.stringify(input.behaviors ?? []), input.expertiseLevel ?? "intermediate", JSON.stringify(input.demographics ?? {}), JSON.stringify(input.painPoints ?? []), input.metadata ? JSON.stringify(input.metadata) : "{}", input.enabled === false ? 0 : 1, input.authEmail ?? null, input.authPassword ?? null, input.authLoginPath ?? null, timestamp, timestamp);
17662
+ INSERT INTO personas (id, short_id, project_id, name, description, role, instructions, traits, goals, behaviors, expertise_level, demographics, pain_points, metadata, enabled, auth_email, auth_password, auth_login_path, auth_cookies, auth_strategy, auth_headers, auth_script, version, created_at, updated_at)
17663
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?, ?)
17664
+ `).run(id, short_id, input.projectId ?? null, input.name, input.description ?? "", input.role, input.instructions ?? "", JSON.stringify(input.traits ?? []), JSON.stringify(input.goals ?? []), JSON.stringify(input.behaviors ?? []), input.expertiseLevel ?? "intermediate", JSON.stringify(input.demographics ?? {}), JSON.stringify(input.painPoints ?? []), input.metadata ? JSON.stringify(input.metadata) : "{}", input.enabled === false ? 0 : 1, input.authEmail ?? null, input.authPassword ?? null, input.authLoginPath ?? null, null, input.authStrategy ?? "form-login", input.authHeaders ? JSON.stringify(input.authHeaders) : null, input.authCustomScript ?? null, timestamp, timestamp);
17515
17665
  return getPersona(id);
17516
17666
  }
17517
17667
  function getPersona(id) {
@@ -17629,6 +17779,18 @@ function updatePersona(id, updates, version) {
17629
17779
  sets.push("auth_cookies = ?");
17630
17780
  params.push(updates.authCookies ? JSON.stringify(updates.authCookies) : null);
17631
17781
  }
17782
+ if (updates.authStrategy !== undefined) {
17783
+ sets.push("auth_strategy = ?");
17784
+ params.push(updates.authStrategy);
17785
+ }
17786
+ if (updates.authHeaders !== undefined) {
17787
+ sets.push("auth_headers = ?");
17788
+ params.push(JSON.stringify(updates.authHeaders));
17789
+ }
17790
+ if (updates.authCustomScript !== undefined) {
17791
+ sets.push("auth_script = ?");
17792
+ params.push(updates.authCustomScript);
17793
+ }
17632
17794
  if (sets.length === 0) {
17633
17795
  return existing;
17634
17796
  }
@@ -18166,6 +18328,24 @@ function signPayload(body, secret) {
18166
18328
  }
18167
18329
  return `sha256=${Math.abs(hash).toString(16).padStart(16, "0")}`;
18168
18330
  }
18331
+ function formatDiscordPayload(payload) {
18332
+ const isPassed = payload.run.status === "passed";
18333
+ const color = isPassed ? 2278750 : 15680580;
18334
+ return {
18335
+ username: "open-testers",
18336
+ embeds: [
18337
+ {
18338
+ title: `Test Run ${payload.run.status.toUpperCase()}`,
18339
+ color,
18340
+ description: `URL: ${payload.run.url}
18341
+ ` + `Results: ${payload.run.passed}/${payload.run.total} passed` + (payload.run.failed > 0 ? ` (${payload.run.failed} failed)` : "") + (payload.schedule ? `
18342
+ Schedule: ${payload.schedule.name}` : ""),
18343
+ timestamp: payload.timestamp,
18344
+ footer: { text: "open-testers" }
18345
+ }
18346
+ ]
18347
+ };
18348
+ }
18169
18349
  function formatSlackPayload(payload) {
18170
18350
  const status = payload.run.status === "passed" ? ":white_check_mark:" : ":x:";
18171
18351
  const color = payload.run.status === "passed" ? "#22c55e" : "#ef4444";
@@ -18208,7 +18388,8 @@ async function dispatchWebhooks(event, run, schedule) {
18208
18388
  if (!webhook.events.includes(event) && !webhook.events.includes("*"))
18209
18389
  continue;
18210
18390
  const isSlack = webhook.url.includes("hooks.slack.com");
18211
- const body = isSlack ? JSON.stringify(formatSlackPayload(payload)) : JSON.stringify(payload);
18391
+ const isDiscord = webhook.url.includes("discord.com/api/webhooks") || webhook.url.includes("discordapp.com/api/webhooks");
18392
+ const body = isSlack ? JSON.stringify(formatSlackPayload(payload)) : isDiscord ? JSON.stringify(formatDiscordPayload(payload)) : JSON.stringify(payload);
18212
18393
  const headers = {
18213
18394
  "Content-Type": "application/json"
18214
18395
  };
@@ -18529,13 +18710,35 @@ async function runSingleScenario(scenario, runId, options) {
18529
18710
  emit({ type: "scenario:start", scenarioId: scenario.id, scenarioName: scenario.name, resultId: result.id, runId });
18530
18711
  let browser = null;
18531
18712
  let page = null;
18713
+ let context = null;
18714
+ let harPath = null;
18532
18715
  let stopNetworkLogging = null;
18533
18716
  const networkErrors = [];
18534
18717
  try {
18535
18718
  browser = await launchBrowser({ headless: !(effectiveOptions.headed ?? false), engine: effectiveOptions.engine });
18536
- page = await getPage(browser, {
18537
- viewport: config.browser.viewport
18538
- });
18719
+ const useHar = effectiveOptions.engine !== "lightpanda" && effectiveOptions.engine !== "bun";
18720
+ if (useHar) {
18721
+ const testersDir = process.env["HASNA_TESTERS_DIR"] || join12(process.env["HOME"] || "", ".hasna", "testers");
18722
+ const harDir = join12(testersDir, "hars");
18723
+ mkdirSync6(harDir, { recursive: true });
18724
+ harPath = join12(harDir, `${result.id}.har`);
18725
+ const contextOptions = {
18726
+ viewport: config.browser.viewport,
18727
+ recordHar: { path: harPath, mode: "full" }
18728
+ };
18729
+ if (effectiveOptions.recordVideo) {
18730
+ const videoDir = join12(testersDir, "videos");
18731
+ mkdirSync6(videoDir, { recursive: true });
18732
+ contextOptions.recordVideo = { dir: videoDir, size: config.browser.viewport };
18733
+ }
18734
+ context = await browser.newContext(contextOptions);
18735
+ page = await context.newPage();
18736
+ } else {
18737
+ page = await getPage(browser, {
18738
+ viewport: config.browser.viewport,
18739
+ engine: effectiveOptions.engine
18740
+ });
18741
+ }
18539
18742
  const targetUrl = scenario.targetPath ? `${options.url.replace(/\/$/, "")}${scenario.targetPath}` : options.url;
18540
18743
  const scenarioTimeout = scenario.timeoutMs ?? options.timeout ?? config.browser.timeout ?? 60000;
18541
18744
  registerSession({
@@ -18555,7 +18758,11 @@ async function runSingleScenario(scenario, runId, options) {
18555
18758
  }
18556
18759
  });
18557
18760
  const consoleErrors = [];
18761
+ const consoleLogs = [];
18762
+ let currentStep = 0;
18558
18763
  page.on("console", (msg) => {
18764
+ const logEntry = { step: currentStep > 0 ? currentStep : null, type: msg.type(), text: msg.text(), timestamp: Date.now() };
18765
+ consoleLogs.push(logEntry);
18559
18766
  if (msg.type() === "error")
18560
18767
  consoleErrors.push(msg.text());
18561
18768
  });
@@ -18587,6 +18794,7 @@ async function runSingleScenario(scenario, runId, options) {
18587
18794
  }
18588
18795
  await page.goto(targetUrl, { timeout: Math.min(scenarioTimeout, 30000) });
18589
18796
  const stepStartTimes = new Map;
18797
+ const stepResultIds = new Map;
18590
18798
  const agentResult = await withTimeout(runAgentLoop({
18591
18799
  client,
18592
18800
  page,
@@ -18609,13 +18817,32 @@ async function runSingleScenario(scenario, runId, options) {
18609
18817
  onStep: (stepEvent) => {
18610
18818
  let stepDurationMs;
18611
18819
  if (stepEvent.type === "tool_call") {
18820
+ currentStep = stepEvent.stepNumber;
18612
18821
  stepStartTimes.set(stepEvent.stepNumber, Date.now());
18822
+ const stepResult = createStepResult({
18823
+ resultId: result.id,
18824
+ stepNumber: stepEvent.stepNumber,
18825
+ action: stepEvent.toolName ?? `step-${stepEvent.stepNumber}`,
18826
+ toolName: stepEvent.toolName,
18827
+ toolInput: stepEvent.toolInput,
18828
+ thinking: stepEvent.thinking
18829
+ });
18830
+ stepResultIds.set(stepEvent.stepNumber, stepResult.id);
18613
18831
  } else if (stepEvent.type === "tool_result") {
18614
18832
  const startTime = stepStartTimes.get(stepEvent.stepNumber);
18615
18833
  if (startTime !== undefined) {
18616
18834
  stepDurationMs = Date.now() - startTime;
18617
18835
  stepStartTimes.delete(stepEvent.stepNumber);
18618
18836
  }
18837
+ const stepResultId = stepResultIds.get(stepEvent.stepNumber);
18838
+ if (stepResultId) {
18839
+ const isSuccess = !stepEvent.toolResult?.toLowerCase().includes("error") && !stepEvent.toolResult?.toLowerCase().includes("failed");
18840
+ updateStepResult(stepResultId, {
18841
+ status: isSuccess ? "passed" : "failed",
18842
+ toolResult: stepEvent.toolResult,
18843
+ durationMs: stepDurationMs
18844
+ });
18845
+ }
18619
18846
  }
18620
18847
  emit({
18621
18848
  type: `step:${stepEvent.type}`,
@@ -18664,7 +18891,7 @@ async function runSingleScenario(scenario, runId, options) {
18664
18891
  durationMs: Date.now() - new Date(result.createdAt).getTime(),
18665
18892
  tokensUsed: agentResult.tokensUsed,
18666
18893
  costCents: estimateCost(model, agentResult.tokensUsed),
18667
- metadata: networkErrors.length > 0 ? networkMeta : undefined
18894
+ metadata: { consoleLogs, ...networkErrors.length > 0 ? networkMeta : {} }
18668
18895
  });
18669
18896
  if (agentResult.status === "failed" || agentResult.status === "error") {
18670
18897
  const failureAnalysis = analyzeFailure(null, agentResult.reasoning ?? null);
@@ -18700,8 +18927,16 @@ async function runSingleScenario(scenario, runId, options) {
18700
18927
  emit({ type: "scenario:error", scenarioId: scenario.id, scenarioName: scenario.name, error: errorMsg, runId });
18701
18928
  return updatedResult;
18702
18929
  } finally {
18703
- if (browser)
18704
- await closeBrowser(browser, effectiveOptions.engine);
18930
+ if (harPath) {
18931
+ try {
18932
+ updateResult(result.id, { metadata: { harPath } });
18933
+ } catch {}
18934
+ }
18935
+ if (browser) {
18936
+ try {
18937
+ await closeBrowser(browser, effectiveOptions.engine);
18938
+ } catch {}
18939
+ }
18705
18940
  }
18706
18941
  }
18707
18942
  async function runBatch(scenarios, options) {
@@ -20229,7 +20464,7 @@ async function handleRequest(req) {
20229
20464
  if (pathname === "/api/status" && method === "GET") {
20230
20465
  const config = loadConfig();
20231
20466
  getDatabase();
20232
- const dbPath = process.env["HASNA_TESTERS_DB_PATH"] ?? process.env["TESTERS_DB_PATH"] ?? join12(getTestersDir(), "testers.db");
20467
+ const dbPath = process.env["HASNA_TESTERS_DB_PATH"] ?? process.env["TESTERS_DB_PATH"] ?? join13(getTestersDir(), "testers.db");
20233
20468
  const scenarios = listScenarios();
20234
20469
  const runs = listRuns();
20235
20470
  return jsonResponse({
@@ -20888,7 +21123,7 @@ async function handleRequest(req) {
20888
21123
  return jsonResponse({ routes, apiRoutes, totalCovered: coverageMap.size });
20889
21124
  }
20890
21125
  if (!pathname.startsWith("/api")) {
20891
- const dashboardDir = join12(import.meta.dir, "..", "..", "dashboard", "dist");
21126
+ const dashboardDir = join13(import.meta.dir, "..", "..", "dashboard", "dist");
20892
21127
  if (!existsSync10(dashboardDir)) {
20893
21128
  return new Response(`<!DOCTYPE html>
20894
21129
  <html>
@@ -20907,7 +21142,7 @@ async function handleRequest(req) {
20907
21142
  }
20908
21143
  });
20909
21144
  }
20910
- const filePath = join12(dashboardDir, pathname === "/" ? "index.html" : pathname);
21145
+ const filePath = join13(dashboardDir, pathname === "/" ? "index.html" : pathname);
20911
21146
  if (existsSync10(filePath)) {
20912
21147
  const file = Bun.file(filePath);
20913
21148
  return new Response(file, {
@@ -20917,7 +21152,7 @@ async function handleRequest(req) {
20917
21152
  }
20918
21153
  });
20919
21154
  }
20920
- const indexPath = join12(dashboardDir, "index.html");
21155
+ const indexPath = join13(dashboardDir, "index.html");
20921
21156
  if (existsSync10(indexPath)) {
20922
21157
  const file = Bun.file(indexPath);
20923
21158
  return new Response(file, {
@@ -20931,8 +21166,19 @@ async function handleRequest(req) {
20931
21166
  return errorResponse("Not found", 404);
20932
21167
  }
20933
21168
  var port = parseInt(process.env["TESTERS_PORT"] ?? "19450", 10);
21169
+ process.on("unhandledRejection", (reason) => {
21170
+ const msg = reason instanceof Error ? reason.stack ?? reason.message : String(reason);
21171
+ console.error(`[testers-serve] Unhandled promise rejection: ${msg}`);
21172
+ });
21173
+ process.on("uncaughtException", (err) => {
21174
+ console.error(`[testers-serve] Uncaught exception: ${err.stack ?? err.message}`);
21175
+ });
20934
21176
  var server = Bun.serve({
20935
21177
  port,
20936
- fetch: handleRequest
21178
+ fetch: handleRequest,
21179
+ error(err) {
21180
+ console.error(`[testers-serve] Request error: ${err.stack ?? err.message}`);
21181
+ return errorResponse("Internal server error", 500);
21182
+ }
20937
21183
  });
20938
21184
  console.log(`Open Testers server running at http://localhost:${server.port}`);