@hasna/testers 0.0.12 → 0.0.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,13 +1,17 @@
1
1
  #!/usr/bin/env bun
2
2
  // @bun
3
3
  var __defProp = Object.defineProperty;
4
+ var __returnValue = (v) => v;
5
+ function __exportSetter(name, newValue) {
6
+ this[name] = __returnValue.bind(null, newValue);
7
+ }
4
8
  var __export = (target, all) => {
5
9
  for (var name in all)
6
10
  __defProp(target, name, {
7
11
  get: all[name],
8
12
  enumerable: true,
9
13
  configurable: true,
10
- set: (newValue) => all[name] = () => newValue
14
+ set: __exportSetter.bind(all, name)
11
15
  });
12
16
  };
13
17
  var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
@@ -118,7 +122,7 @@ function flowFromRow(row) {
118
122
  updatedAt: row.updated_at
119
123
  };
120
124
  }
121
- var MODEL_MAP, VersionConflictError, BrowserError, AIClientError, ScheduleNotFoundError, DependencyCycleError;
125
+ var MODEL_MAP, VersionConflictError, BrowserError, AIClientError, TodosConnectionError, ScheduleNotFoundError, DependencyCycleError;
122
126
  var init_types = __esm(() => {
123
127
  MODEL_MAP = {
124
128
  quick: "claude-haiku-4-5-20251001",
@@ -143,6 +147,12 @@ var init_types = __esm(() => {
143
147
  this.name = "AIClientError";
144
148
  }
145
149
  };
150
+ TodosConnectionError = class TodosConnectionError extends Error {
151
+ constructor(message) {
152
+ super(message);
153
+ this.name = "TodosConnectionError";
154
+ }
155
+ };
146
156
  ScheduleNotFoundError = class ScheduleNotFoundError extends Error {
147
157
  constructor(id) {
148
158
  super(`Schedule not found: ${id}`);
@@ -736,9 +746,9 @@ var init_flows = __esm(() => {
736
746
  });
737
747
 
738
748
  // src/server/index.ts
739
- import { existsSync as existsSync4 } from "fs";
740
- import { join as join4 } from "path";
741
- import { homedir as homedir4 } from "os";
749
+ import { existsSync as existsSync5 } from "fs";
750
+ import { join as join5 } from "path";
751
+ import { homedir as homedir5 } from "os";
742
752
 
743
753
  // node_modules/zod/v3/external.js
744
754
  var exports_external = {};
@@ -6281,6 +6291,119 @@ async function pushFailedRunToLogs(run, failedResults, scenarios) {
6281
6291
  } catch {}
6282
6292
  }
6283
6293
 
6294
+ // src/lib/todos-connector.ts
6295
+ import { Database as Database2 } from "bun:sqlite";
6296
+ import { existsSync as existsSync4 } from "fs";
6297
+ import { join as join4 } from "path";
6298
+ import { homedir as homedir4 } from "os";
6299
+ init_types();
6300
+ function resolveTodosDbPath() {
6301
+ const envPath = process.env["TODOS_DB_PATH"];
6302
+ if (envPath)
6303
+ return envPath;
6304
+ return join4(homedir4(), ".todos", "todos.db");
6305
+ }
6306
+ function connectToTodos() {
6307
+ const dbPath = resolveTodosDbPath();
6308
+ if (!existsSync4(dbPath)) {
6309
+ throw new TodosConnectionError(`Todos database not found at ${dbPath}. Install @hasna/todos or set TODOS_DB_PATH.`);
6310
+ }
6311
+ const db2 = new Database2(dbPath, { readonly: true });
6312
+ db2.exec("PRAGMA foreign_keys = ON");
6313
+ return db2;
6314
+ }
6315
+
6316
+ // src/lib/failure-pipeline.ts
6317
+ async function createFailureTasks(run, failedResults, scenarios) {
6318
+ if (failedResults.length === 0)
6319
+ return { created: 0, skipped: 0 };
6320
+ const projectId = process.env["TESTERS_TODOS_PROJECT_ID"];
6321
+ if (!projectId)
6322
+ return { created: 0, skipped: 0 };
6323
+ let db2 = null;
6324
+ try {
6325
+ db2 = connectToTodos();
6326
+ } catch {
6327
+ return { created: 0, skipped: 0 };
6328
+ }
6329
+ const scenarioMap = new Map(scenarios.map((s) => [s.id, s]));
6330
+ let created = 0;
6331
+ let skipped = 0;
6332
+ try {
6333
+ for (const result of failedResults) {
6334
+ const scenario = scenarioMap.get(result.scenarioId);
6335
+ const title = `BUG: [testers] ${scenario?.name ?? result.scenarioId} failed`;
6336
+ const existing = db2.query("SELECT id FROM tasks WHERE title = ? AND status NOT IN ('completed', 'cancelled') LIMIT 1").get(title);
6337
+ if (existing) {
6338
+ skipped++;
6339
+ continue;
6340
+ }
6341
+ const id = crypto.randomUUID();
6342
+ const now2 = new Date().toISOString();
6343
+ const description = [
6344
+ `Test failure detected by open-testers.`,
6345
+ ``,
6346
+ `**Run:** ${run.id}`,
6347
+ `**URL:** ${run.url}`,
6348
+ `**Scenario:** ${scenario?.name ?? result.scenarioId}`,
6349
+ `**Status:** ${result.status}`,
6350
+ result.error ? `**Error:** ${result.error}` : null,
6351
+ result.reasoning ? `**Reasoning:** ${result.reasoning.slice(0, 500)}` : null,
6352
+ `**Duration:** ${result.durationMs ? `${(result.durationMs / 1000).toFixed(1)}s` : "N/A"}`,
6353
+ `**Tokens:** ${result.tokensUsed ?? 0}`
6354
+ ].filter(Boolean).join(`
6355
+ `);
6356
+ try {
6357
+ db2.query(`
6358
+ INSERT INTO tasks (id, short_id, title, description, status, priority, tags, project_id, version, created_at, updated_at)
6359
+ VALUES (?, ?, ?, ?, 'pending', 'high', ?, ?, 1, ?, ?)
6360
+ `).run(id, `BUG-${id.slice(0, 6)}`, title, description, JSON.stringify(["bug", "testers", "auto-created"]), projectId, now2, now2);
6361
+ created++;
6362
+ } catch {
6363
+ skipped++;
6364
+ }
6365
+ }
6366
+ } finally {
6367
+ db2.close();
6368
+ }
6369
+ return { created, skipped };
6370
+ }
6371
+ async function notifyFailureToConversations(run, failedResults, scenarios) {
6372
+ const baseUrl = process.env["TESTERS_CONVERSATIONS_URL"];
6373
+ const space = process.env["TESTERS_CONVERSATIONS_SPACE"];
6374
+ if (!baseUrl || !space)
6375
+ return;
6376
+ const scenarioMap = new Map(scenarios.map((s) => [s.id, s]));
6377
+ const total = run.total;
6378
+ const failedCount = failedResults.length;
6379
+ const passedCount = run.passed;
6380
+ const failureLines = failedResults.slice(0, 5).map((r) => {
6381
+ const name = scenarioMap.get(r.scenarioId)?.name ?? r.scenarioId;
6382
+ const err = r.error ? ` \u2014 ${r.error.slice(0, 120)}` : "";
6383
+ return ` \u274C ${name}${err}`;
6384
+ });
6385
+ const extra = failedResults.length > 5 ? ` \u2026 and ${failedResults.length - 5} more` : "";
6386
+ const message = [
6387
+ `\uD83D\uDEA8 **Testers run failed** \u2014 ${failedCount}/${total} scenarios failed`,
6388
+ ``,
6389
+ `**URL:** ${run.url}`,
6390
+ `**Run ID:** \`${run.id}\``,
6391
+ `**Pass rate:** ${passedCount}/${total}`,
6392
+ ``,
6393
+ `**Failures:**`,
6394
+ ...failureLines,
6395
+ extra
6396
+ ].filter((l) => l !== "").join(`
6397
+ `);
6398
+ try {
6399
+ await fetch(`${baseUrl.replace(/\/$/, "")}/api/spaces/${encodeURIComponent(space)}/messages`, {
6400
+ method: "POST",
6401
+ headers: { "Content-Type": "application/json" },
6402
+ body: JSON.stringify({ content: message, from: "testers" })
6403
+ });
6404
+ } catch {}
6405
+ }
6406
+
6284
6407
  // src/lib/runner.ts
6285
6408
  var eventHandler = null;
6286
6409
  function emit(event) {
@@ -6514,6 +6637,8 @@ async function runBatch(scenarios, options) {
6514
6637
  if (finalRun.status === "failed") {
6515
6638
  const failedResults = results.filter((r) => r.status === "failed" || r.status === "error");
6516
6639
  pushFailedRunToLogs(finalRun, failedResults, scenarios).catch(() => {});
6640
+ createFailureTasks(finalRun, failedResults, scenarios).catch(() => {});
6641
+ notifyFailureToConversations(finalRun, failedResults, scenarios).catch(() => {});
6517
6642
  }
6518
6643
  return { run: finalRun, results };
6519
6644
  }
@@ -7003,7 +7128,7 @@ async function handleRequest(req) {
7003
7128
  if (pathname === "/api/status" && method === "GET") {
7004
7129
  const config = loadConfig();
7005
7130
  getDatabase();
7006
- const dbPath = process.env["TESTERS_DB_PATH"] ?? join4(homedir4(), ".testers", "testers.db");
7131
+ const dbPath = process.env["TESTERS_DB_PATH"] ?? join5(homedir5(), ".testers", "testers.db");
7007
7132
  const scenarios = listScenarios();
7008
7133
  const runs = listRuns();
7009
7134
  return jsonResponse({
@@ -7198,7 +7323,7 @@ async function handleRequest(req) {
7198
7323
  const screenshot = getScreenshot(id);
7199
7324
  if (!screenshot)
7200
7325
  return errorResponse("Screenshot not found", 404);
7201
- if (!existsSync4(screenshot.filePath)) {
7326
+ if (!existsSync5(screenshot.filePath)) {
7202
7327
  return errorResponse("Screenshot file not found on disk", 404);
7203
7328
  }
7204
7329
  const file = Bun.file(screenshot.filePath);
@@ -7253,8 +7378,8 @@ async function handleRequest(req) {
7253
7378
  return jsonResponse({ deleted: true });
7254
7379
  }
7255
7380
  if (!pathname.startsWith("/api")) {
7256
- const dashboardDir = join4(import.meta.dir, "..", "..", "dashboard", "dist");
7257
- if (!existsSync4(dashboardDir)) {
7381
+ const dashboardDir = join5(import.meta.dir, "..", "..", "dashboard", "dist");
7382
+ if (!existsSync5(dashboardDir)) {
7258
7383
  return new Response(`<!DOCTYPE html>
7259
7384
  <html>
7260
7385
  <head><title>Open Testers</title></head>
@@ -7272,8 +7397,8 @@ async function handleRequest(req) {
7272
7397
  }
7273
7398
  });
7274
7399
  }
7275
- const filePath = join4(dashboardDir, pathname === "/" ? "index.html" : pathname);
7276
- if (existsSync4(filePath)) {
7400
+ const filePath = join5(dashboardDir, pathname === "/" ? "index.html" : pathname);
7401
+ if (existsSync5(filePath)) {
7277
7402
  const file = Bun.file(filePath);
7278
7403
  return new Response(file, {
7279
7404
  headers: {
@@ -7282,8 +7407,8 @@ async function handleRequest(req) {
7282
7407
  }
7283
7408
  });
7284
7409
  }
7285
- const indexPath = join4(dashboardDir, "index.html");
7286
- if (existsSync4(indexPath)) {
7410
+ const indexPath = join5(dashboardDir, "index.html");
7411
+ if (existsSync5(indexPath)) {
7287
7412
  const file = Bun.file(indexPath);
7288
7413
  return new Response(file, {
7289
7414
  headers: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/testers",
3
- "version": "0.0.12",
3
+ "version": "0.0.13",
4
4
  "description": "AI-powered QA testing CLI — spawns cheap AI agents to test web apps with headless browsers",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -1,3 +0,0 @@
1
- #!/usr/bin/env bun
2
- export {};
3
- //# sourceMappingURL=index.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/cli/index.tsx"],"names":[],"mappings":""}
@@ -1,3 +0,0 @@
1
- #!/usr/bin/env bun
2
- export {};
3
- //# sourceMappingURL=index.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/mcp/index.ts"],"names":[],"mappings":""}