@hasna/testers 0.0.28 → 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.
- package/LICENSE +1 -154
- package/README.md +60 -0
- package/dist/cli/index.js +944 -76
- package/dist/db/database.d.ts.map +1 -1
- package/dist/db/personas.d.ts.map +1 -1
- package/dist/db/runs.d.ts +29 -0
- package/dist/db/runs.d.ts.map +1 -1
- package/dist/db/scenarios.d.ts +12 -0
- package/dist/db/scenarios.d.ts.map +1 -1
- package/dist/db/sessions.d.ts +36 -0
- package/dist/db/sessions.d.ts.map +1 -0
- package/dist/db/step-results.d.ts +30 -0
- package/dist/db/step-results.d.ts.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +439 -24
- package/dist/lib/a11y-audit.d.ts +54 -0
- package/dist/lib/a11y-audit.d.ts.map +1 -0
- package/dist/lib/api-discovery.d.ts +46 -0
- package/dist/lib/api-discovery.d.ts.map +1 -0
- package/dist/lib/assertions.d.ts.map +1 -1
- package/dist/lib/auth-profiles.d.ts +16 -0
- package/dist/lib/auth-profiles.d.ts.map +1 -0
- package/dist/lib/auth-session-pool.d.ts +57 -0
- package/dist/lib/auth-session-pool.d.ts.map +1 -0
- package/dist/lib/batch-actions.d.ts +44 -0
- package/dist/lib/batch-actions.d.ts.map +1 -0
- package/dist/lib/browser-compat.d.ts +14 -0
- package/dist/lib/browser-compat.d.ts.map +1 -0
- package/dist/lib/browser.d.ts +7 -8
- package/dist/lib/browser.d.ts.map +1 -1
- package/dist/lib/ci.d.ts +12 -0
- package/dist/lib/ci.d.ts.map +1 -1
- package/dist/lib/discovery.d.ts +23 -0
- package/dist/lib/discovery.d.ts.map +1 -0
- package/dist/lib/dom-mutation.d.ts +53 -0
- package/dist/lib/dom-mutation.d.ts.map +1 -0
- package/dist/lib/environment.d.ts +26 -0
- package/dist/lib/environment.d.ts.map +1 -0
- package/dist/lib/health-scan.d.ts +2 -1
- package/dist/lib/health-scan.d.ts.map +1 -1
- package/dist/lib/junit-export.d.ts +24 -0
- package/dist/lib/junit-export.d.ts.map +1 -0
- package/dist/lib/network-mock.d.ts +38 -0
- package/dist/lib/network-mock.d.ts.map +1 -0
- package/dist/lib/offline-mode.d.ts +31 -0
- package/dist/lib/offline-mode.d.ts.map +1 -0
- package/dist/lib/pdf-export.d.ts +27 -0
- package/dist/lib/pdf-export.d.ts.map +1 -0
- package/dist/lib/performance.d.ts +65 -0
- package/dist/lib/performance.d.ts.map +1 -0
- package/dist/lib/pr-comment.d.ts +27 -0
- package/dist/lib/pr-comment.d.ts.map +1 -0
- package/dist/lib/preview-detect.d.ts +27 -0
- package/dist/lib/preview-detect.d.ts.map +1 -0
- package/dist/lib/recorder.d.ts +42 -0
- package/dist/lib/recorder.d.ts.map +1 -1
- package/dist/lib/repo-discovery.d.ts +102 -0
- package/dist/lib/repo-discovery.d.ts.map +1 -0
- package/dist/lib/repo-executor.d.ts +56 -0
- package/dist/lib/repo-executor.d.ts.map +1 -0
- package/dist/lib/responsive.d.ts +43 -0
- package/dist/lib/responsive.d.ts.map +1 -0
- package/dist/lib/runner.d.ts +1 -0
- package/dist/lib/runner.d.ts.map +1 -1
- package/dist/lib/scenario-chain.d.ts +52 -0
- package/dist/lib/scenario-chain.d.ts.map +1 -0
- package/dist/lib/templates.d.ts.map +1 -1
- package/dist/lib/webhooks.d.ts +3 -0
- package/dist/lib/webhooks.d.ts.map +1 -1
- package/dist/mcp/index.js +491 -38
- package/dist/sdk/index.d.ts +47 -0
- package/dist/sdk/index.d.ts.map +1 -0
- package/dist/server/index.js +274 -28
- package/dist/types/index.d.ts +64 -2
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -65,7 +65,8 @@ function scenarioFromRow(row) {
|
|
|
65
65
|
createdAt: row.created_at,
|
|
66
66
|
updatedAt: row.updated_at,
|
|
67
67
|
lastPassedAt: row.last_passed_at ?? null,
|
|
68
|
-
lastPassedUrl: row.last_passed_url ?? null
|
|
68
|
+
lastPassedUrl: row.last_passed_url ?? null,
|
|
69
|
+
parameters: row.parameters ? JSON.parse(row.parameters) : null
|
|
69
70
|
};
|
|
70
71
|
}
|
|
71
72
|
function runFromRow(row) {
|
|
@@ -85,7 +86,14 @@ function runFromRow(row) {
|
|
|
85
86
|
metadata: row.metadata ? JSON.parse(row.metadata) : null,
|
|
86
87
|
isBaseline: row.is_baseline === 1,
|
|
87
88
|
samples: row.samples ?? 1,
|
|
88
|
-
flakinessThreshold: row.flakiness_threshold ?? 0.95
|
|
89
|
+
flakinessThreshold: row.flakiness_threshold ?? 0.95,
|
|
90
|
+
prNumber: row.pr_number ?? null,
|
|
91
|
+
prTitle: row.pr_title ?? null,
|
|
92
|
+
prBranch: row.pr_branch ?? null,
|
|
93
|
+
prBaseBranch: row.pr_base_branch ?? null,
|
|
94
|
+
prCommitSha: row.pr_commit_sha ?? null,
|
|
95
|
+
prUrl: row.pr_url ?? null,
|
|
96
|
+
ghAppInstallationId: row.gh_app_installation_id ?? null
|
|
89
97
|
};
|
|
90
98
|
}
|
|
91
99
|
function resultFromRow(row) {
|
|
@@ -106,7 +114,8 @@ function resultFromRow(row) {
|
|
|
106
114
|
createdAt: row.created_at,
|
|
107
115
|
personaId: row.persona_id ?? null,
|
|
108
116
|
personaName: row.persona_name ?? null,
|
|
109
|
-
failureAnalysis: row.failure_analysis ? JSON.parse(row.failure_analysis) : null
|
|
117
|
+
failureAnalysis: row.failure_analysis ? JSON.parse(row.failure_analysis) : null,
|
|
118
|
+
harPath: row.har_path ?? null
|
|
110
119
|
};
|
|
111
120
|
}
|
|
112
121
|
function screenshotFromRow(row) {
|
|
@@ -180,7 +189,10 @@ function personaFromRow(row) {
|
|
|
180
189
|
email: row.auth_email,
|
|
181
190
|
password: row.auth_password,
|
|
182
191
|
loginPath: row.auth_login_path ?? "/login",
|
|
183
|
-
cookies: row.auth_cookies ? JSON.parse(row.auth_cookies) : null
|
|
192
|
+
cookies: row.auth_cookies ? JSON.parse(row.auth_cookies) : null,
|
|
193
|
+
strategy: row.auth_strategy ?? "form-login",
|
|
194
|
+
headers: row.auth_headers ? JSON.parse(row.auth_headers) : undefined,
|
|
195
|
+
customScript: row.auth_script ?? undefined
|
|
184
196
|
} : null
|
|
185
197
|
};
|
|
186
198
|
}
|
|
@@ -10122,6 +10134,43 @@ ALTER TABLE scenarios ADD COLUMN required_role TEXT;
|
|
|
10122
10134
|
machine_id TEXT,
|
|
10123
10135
|
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
10124
10136
|
);
|
|
10137
|
+
`,
|
|
10138
|
+
`
|
|
10139
|
+
ALTER TABLE results ADD COLUMN har_path TEXT;
|
|
10140
|
+
`,
|
|
10141
|
+
`
|
|
10142
|
+
ALTER TABLE scenarios ADD COLUMN parameters TEXT;
|
|
10143
|
+
`,
|
|
10144
|
+
`
|
|
10145
|
+
ALTER TABLE personas ADD COLUMN auth_strategy TEXT DEFAULT 'form-login';
|
|
10146
|
+
ALTER TABLE personas ADD COLUMN auth_headers TEXT;
|
|
10147
|
+
ALTER TABLE personas ADD COLUMN auth_script TEXT;
|
|
10148
|
+
`,
|
|
10149
|
+
`
|
|
10150
|
+
CREATE TABLE IF NOT EXISTS step_results (
|
|
10151
|
+
id TEXT PRIMARY KEY,
|
|
10152
|
+
result_id TEXT NOT NULL REFERENCES results(id) ON DELETE CASCADE,
|
|
10153
|
+
step_number INTEGER NOT NULL,
|
|
10154
|
+
action TEXT NOT NULL,
|
|
10155
|
+
status TEXT NOT NULL DEFAULT 'running' CHECK(status IN ('passed','failed','error','running','skipped')),
|
|
10156
|
+
tool_name TEXT,
|
|
10157
|
+
tool_input TEXT,
|
|
10158
|
+
tool_result TEXT,
|
|
10159
|
+
thinking TEXT,
|
|
10160
|
+
error TEXT,
|
|
10161
|
+
duration_ms INTEGER,
|
|
10162
|
+
screenshot_id TEXT REFERENCES screenshots(id),
|
|
10163
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
10164
|
+
);
|
|
10165
|
+
`,
|
|
10166
|
+
`
|
|
10167
|
+
ALTER TABLE runs ADD COLUMN pr_number INTEGER;
|
|
10168
|
+
ALTER TABLE runs ADD COLUMN pr_title TEXT;
|
|
10169
|
+
ALTER TABLE runs ADD COLUMN pr_branch TEXT;
|
|
10170
|
+
ALTER TABLE runs ADD COLUMN pr_base_branch TEXT;
|
|
10171
|
+
ALTER TABLE runs ADD COLUMN pr_commit_sha TEXT;
|
|
10172
|
+
ALTER TABLE runs ADD COLUMN pr_url TEXT;
|
|
10173
|
+
ALTER TABLE runs ADD COLUMN gh_app_installation_id TEXT;
|
|
10125
10174
|
`
|
|
10126
10175
|
];
|
|
10127
10176
|
});
|
|
@@ -10130,8 +10179,12 @@ ALTER TABLE scenarios ADD COLUMN required_role TEXT;
|
|
|
10130
10179
|
var exports_runs = {};
|
|
10131
10180
|
__export(exports_runs, {
|
|
10132
10181
|
updateRun: () => updateRun,
|
|
10182
|
+
updatePrRunMetadata: () => updatePrRunMetadata,
|
|
10133
10183
|
listRuns: () => listRuns,
|
|
10184
|
+
listPrRuns: () => listPrRuns,
|
|
10185
|
+
getRunsByPr: () => getRunsByPr,
|
|
10134
10186
|
getRun: () => getRun,
|
|
10187
|
+
getLatestPrRun: () => getLatestPrRun,
|
|
10135
10188
|
deleteRun: () => deleteRun,
|
|
10136
10189
|
createRun: () => createRun,
|
|
10137
10190
|
countRuns: () => countRuns
|
|
@@ -10141,9 +10194,9 @@ function createRun(input) {
|
|
|
10141
10194
|
const id = uuid();
|
|
10142
10195
|
const timestamp = now();
|
|
10143
10196
|
db2.query(`
|
|
10144
|
-
INSERT INTO runs (id, project_id, status, url, model, headed, parallel, total, passed, failed, started_at, finished_at, metadata, samples, flakiness_threshold)
|
|
10145
|
-
VALUES (?, ?, 'pending', ?, ?, ?, ?, 0, 0, 0, ?, NULL, ?, ?, ?)
|
|
10146
|
-
`).run(id, input.projectId ?? null, input.url, input.model, input.headed ? 1 : 0, input.parallel ?? 1, timestamp,
|
|
10197
|
+
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)
|
|
10198
|
+
VALUES (?, ?, 'pending', ?, ?, ?, ?, 0, 0, 0, ?, NULL, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
10199
|
+
`).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);
|
|
10147
10200
|
return getRun(id);
|
|
10148
10201
|
}
|
|
10149
10202
|
function getRun(id) {
|
|
@@ -10171,6 +10224,14 @@ function listRuns(filter) {
|
|
|
10171
10224
|
conditions.push("status = ?");
|
|
10172
10225
|
params.push(filter.status);
|
|
10173
10226
|
}
|
|
10227
|
+
if (filter?.since) {
|
|
10228
|
+
conditions.push("started_at >= ?");
|
|
10229
|
+
params.push(filter.since);
|
|
10230
|
+
}
|
|
10231
|
+
if (filter?.until) {
|
|
10232
|
+
conditions.push("started_at <= ?");
|
|
10233
|
+
params.push(filter.until);
|
|
10234
|
+
}
|
|
10174
10235
|
let sql = "SELECT * FROM runs";
|
|
10175
10236
|
if (conditions.length > 0) {
|
|
10176
10237
|
sql += " WHERE " + conditions.join(" AND ");
|
|
@@ -10202,6 +10263,14 @@ function countRuns(filter) {
|
|
|
10202
10263
|
conditions.push("status = ?");
|
|
10203
10264
|
params.push(filter.status);
|
|
10204
10265
|
}
|
|
10266
|
+
if (filter?.since) {
|
|
10267
|
+
conditions.push("started_at >= ?");
|
|
10268
|
+
params.push(filter.since);
|
|
10269
|
+
}
|
|
10270
|
+
if (filter?.until) {
|
|
10271
|
+
conditions.push("started_at <= ?");
|
|
10272
|
+
params.push(filter.until);
|
|
10273
|
+
}
|
|
10205
10274
|
let sql = "SELECT COUNT(*) as count FROM runs";
|
|
10206
10275
|
if (conditions.length > 0)
|
|
10207
10276
|
sql += " WHERE " + conditions.join(" AND ");
|
|
@@ -10279,6 +10348,52 @@ function deleteRun(id) {
|
|
|
10279
10348
|
const result = db2.query("DELETE FROM runs WHERE id = ?").run(run.id);
|
|
10280
10349
|
return result.changes > 0;
|
|
10281
10350
|
}
|
|
10351
|
+
function getRunsByPr(prNumber) {
|
|
10352
|
+
const db2 = getDatabase();
|
|
10353
|
+
const rows = db2.query("SELECT * FROM runs WHERE pr_number = ? ORDER BY started_at DESC").all(prNumber);
|
|
10354
|
+
return rows.map(runFromRow);
|
|
10355
|
+
}
|
|
10356
|
+
function getLatestPrRun(prNumber) {
|
|
10357
|
+
const db2 = getDatabase();
|
|
10358
|
+
const row = db2.query("SELECT * FROM runs WHERE pr_number = ? ORDER BY started_at DESC, rowid DESC LIMIT 1").get(prNumber);
|
|
10359
|
+
return row ? runFromRow(row) : null;
|
|
10360
|
+
}
|
|
10361
|
+
function listPrRuns(filter) {
|
|
10362
|
+
const db2 = getDatabase();
|
|
10363
|
+
const conditions = ["pr_number IS NOT NULL"];
|
|
10364
|
+
const params = [];
|
|
10365
|
+
if (filter?.branch) {
|
|
10366
|
+
conditions.push("pr_branch = ?");
|
|
10367
|
+
params.push(filter.branch);
|
|
10368
|
+
}
|
|
10369
|
+
if (filter?.baseBranch) {
|
|
10370
|
+
conditions.push("pr_base_branch = ?");
|
|
10371
|
+
params.push(filter.baseBranch);
|
|
10372
|
+
}
|
|
10373
|
+
let sql = "SELECT * FROM runs WHERE " + conditions.join(" AND ") + " ORDER BY started_at DESC";
|
|
10374
|
+
if (filter?.limit) {
|
|
10375
|
+
sql += " LIMIT ?";
|
|
10376
|
+
params.push(filter.limit);
|
|
10377
|
+
}
|
|
10378
|
+
if (filter?.offset) {
|
|
10379
|
+
sql += " OFFSET ?";
|
|
10380
|
+
params.push(filter.offset);
|
|
10381
|
+
}
|
|
10382
|
+
const rows = db2.query(sql).all(...params);
|
|
10383
|
+
return rows.map(runFromRow);
|
|
10384
|
+
}
|
|
10385
|
+
function updatePrRunMetadata(runId, prData) {
|
|
10386
|
+
const db2 = getDatabase();
|
|
10387
|
+
const existing = getRun(runId);
|
|
10388
|
+
if (!existing) {
|
|
10389
|
+
throw new Error(`Run not found: ${runId}`);
|
|
10390
|
+
}
|
|
10391
|
+
db2.query(`
|
|
10392
|
+
UPDATE runs SET pr_number = ?, pr_title = ?, pr_branch = ?, pr_base_branch = ?, pr_commit_sha = ?, pr_url = ?, gh_app_installation_id = ?
|
|
10393
|
+
WHERE id = ?
|
|
10394
|
+
`).run(prData.prNumber, prData.prTitle ?? null, prData.prBranch ?? null, prData.prBaseBranch ?? null, prData.prCommitSha ?? null, prData.prUrl ?? null, prData.ghAppInstallationId ?? null, runId);
|
|
10395
|
+
return getRun(runId);
|
|
10396
|
+
}
|
|
10282
10397
|
var init_runs = __esm(() => {
|
|
10283
10398
|
init_types();
|
|
10284
10399
|
init_database();
|
|
@@ -10467,7 +10582,7 @@ function getDefaultConfig() {
|
|
|
10467
10582
|
browser: {
|
|
10468
10583
|
headless: true,
|
|
10469
10584
|
viewport: { width: 1280, height: 720 },
|
|
10470
|
-
timeout:
|
|
10585
|
+
timeout: 120000
|
|
10471
10586
|
},
|
|
10472
10587
|
screenshots: {
|
|
10473
10588
|
dir: join8(getTestersDir(), "screenshots"),
|
|
@@ -11151,6 +11266,16 @@ async function launchBrowser(options) {
|
|
|
11151
11266
|
const headless = options?.headless ?? true;
|
|
11152
11267
|
const viewport = options?.viewport ?? DEFAULT_VIEWPORT;
|
|
11153
11268
|
try {
|
|
11269
|
+
if (engine === "playwright-firefox") {
|
|
11270
|
+
const { firefox } = await import("playwright");
|
|
11271
|
+
const browser = await firefox.launch({ headless });
|
|
11272
|
+
return browser;
|
|
11273
|
+
}
|
|
11274
|
+
if (engine === "playwright-webkit") {
|
|
11275
|
+
const { webkit } = await import("playwright");
|
|
11276
|
+
const browser = await webkit.launch({ headless });
|
|
11277
|
+
return browser;
|
|
11278
|
+
}
|
|
11154
11279
|
return await launchPlaywright({ headless, viewport });
|
|
11155
11280
|
} catch (error) {
|
|
11156
11281
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -11267,8 +11392,9 @@ async function installBrowser(engine) {
|
|
|
11267
11392
|
const { installLightpanda: installLightpanda2 } = await Promise.resolve().then(() => (init_browser_lightpanda(), exports_browser_lightpanda));
|
|
11268
11393
|
return installLightpanda2();
|
|
11269
11394
|
}
|
|
11395
|
+
const browserName = engine === "playwright-firefox" ? "firefox" : engine === "playwright-webkit" ? "webkit" : "chromium";
|
|
11270
11396
|
try {
|
|
11271
|
-
execSync(
|
|
11397
|
+
execSync(`bunx playwright install ${browserName}`, {
|
|
11272
11398
|
stdio: "inherit"
|
|
11273
11399
|
});
|
|
11274
11400
|
} catch (error) {
|
|
@@ -12410,9 +12536,9 @@ function createScenario(input) {
|
|
|
12410
12536
|
const short_id = nextShortId(input.projectId);
|
|
12411
12537
|
const timestamp = now();
|
|
12412
12538
|
db2.query(`
|
|
12413
|
-
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)
|
|
12414
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?, ?)
|
|
12415
|
-
`).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);
|
|
12539
|
+
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)
|
|
12540
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?, ?)
|
|
12541
|
+
`).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);
|
|
12416
12542
|
return getScenario(id);
|
|
12417
12543
|
}
|
|
12418
12544
|
function getScenario(id) {
|
|
@@ -12562,6 +12688,10 @@ function updateScenario(id, input, version) {
|
|
|
12562
12688
|
sets.push("assertions = ?");
|
|
12563
12689
|
params.push(JSON.stringify(input.assertions));
|
|
12564
12690
|
}
|
|
12691
|
+
if (input.parameters !== undefined) {
|
|
12692
|
+
sets.push("parameters = ?");
|
|
12693
|
+
params.push(JSON.stringify(input.parameters));
|
|
12694
|
+
}
|
|
12565
12695
|
if (sets.length === 0) {
|
|
12566
12696
|
return existing;
|
|
12567
12697
|
}
|
|
@@ -13597,6 +13727,8 @@ async function runPipelineScenario(scenario, options) {
|
|
|
13597
13727
|
|
|
13598
13728
|
// src/lib/runner.ts
|
|
13599
13729
|
init_runs();
|
|
13730
|
+
import { mkdirSync as mkdirSync8 } from "fs";
|
|
13731
|
+
import { join as join13 } from "path";
|
|
13600
13732
|
|
|
13601
13733
|
// src/lib/failure-analyzer.ts
|
|
13602
13734
|
function analyzeFailure(error, reasoning) {
|
|
@@ -14378,6 +14510,74 @@ function formatCostsJSON(summary) {
|
|
|
14378
14510
|
return JSON.stringify(summary, null, 2);
|
|
14379
14511
|
}
|
|
14380
14512
|
|
|
14513
|
+
// src/db/step-results.ts
|
|
14514
|
+
init_database();
|
|
14515
|
+
function createStepResult(input) {
|
|
14516
|
+
const db2 = getDatabase();
|
|
14517
|
+
const id = uuid();
|
|
14518
|
+
const timestamp = now();
|
|
14519
|
+
db2.query(`
|
|
14520
|
+
INSERT INTO step_results (id, result_id, step_number, action, status, tool_name, tool_input, thinking, created_at)
|
|
14521
|
+
VALUES (?, ?, ?, ?, 'running', ?, ?, ?, ?)
|
|
14522
|
+
`).run(id, input.resultId, input.stepNumber, input.action, input.toolName ?? null, input.toolInput ? JSON.stringify(input.toolInput) : null, input.thinking ?? null, timestamp);
|
|
14523
|
+
return getStepResult(id);
|
|
14524
|
+
}
|
|
14525
|
+
function getStepResult(id) {
|
|
14526
|
+
const db2 = getDatabase();
|
|
14527
|
+
const row = db2.query("SELECT * FROM step_results WHERE id = ?").get(id);
|
|
14528
|
+
return row ? stepResultFromRow(row) : null;
|
|
14529
|
+
}
|
|
14530
|
+
function updateStepResult(id, updates) {
|
|
14531
|
+
const db2 = getDatabase();
|
|
14532
|
+
const existing = getStepResult(id);
|
|
14533
|
+
if (!existing)
|
|
14534
|
+
return null;
|
|
14535
|
+
const sets = [];
|
|
14536
|
+
const params = [];
|
|
14537
|
+
if (updates.status !== undefined) {
|
|
14538
|
+
sets.push("status = ?");
|
|
14539
|
+
params.push(updates.status);
|
|
14540
|
+
}
|
|
14541
|
+
if (updates.toolResult !== undefined) {
|
|
14542
|
+
sets.push("tool_result = ?");
|
|
14543
|
+
params.push(updates.toolResult);
|
|
14544
|
+
}
|
|
14545
|
+
if (updates.error !== undefined) {
|
|
14546
|
+
sets.push("error = ?");
|
|
14547
|
+
params.push(updates.error);
|
|
14548
|
+
}
|
|
14549
|
+
if (updates.durationMs !== undefined) {
|
|
14550
|
+
sets.push("duration_ms = ?");
|
|
14551
|
+
params.push(updates.durationMs);
|
|
14552
|
+
}
|
|
14553
|
+
if (updates.screenshotId !== undefined) {
|
|
14554
|
+
sets.push("screenshot_id = ?");
|
|
14555
|
+
params.push(updates.screenshotId);
|
|
14556
|
+
}
|
|
14557
|
+
if (sets.length === 0)
|
|
14558
|
+
return existing;
|
|
14559
|
+
params.push(id);
|
|
14560
|
+
db2.query(`UPDATE step_results SET ${sets.join(", ")} WHERE id = ?`).run(...params);
|
|
14561
|
+
return getStepResult(id);
|
|
14562
|
+
}
|
|
14563
|
+
function stepResultFromRow(row) {
|
|
14564
|
+
return {
|
|
14565
|
+
id: row.id,
|
|
14566
|
+
resultId: row.result_id,
|
|
14567
|
+
stepNumber: row.step_number,
|
|
14568
|
+
action: row.action,
|
|
14569
|
+
status: row.status,
|
|
14570
|
+
toolName: row.tool_name,
|
|
14571
|
+
toolInput: row.tool_input ? JSON.parse(row.tool_input) : null,
|
|
14572
|
+
toolResult: row.tool_result,
|
|
14573
|
+
thinking: row.thinking,
|
|
14574
|
+
error: row.error,
|
|
14575
|
+
durationMs: row.duration_ms,
|
|
14576
|
+
screenshotId: row.screenshot_id,
|
|
14577
|
+
createdAt: row.created_at
|
|
14578
|
+
};
|
|
14579
|
+
}
|
|
14580
|
+
|
|
14381
14581
|
// src/db/personas.ts
|
|
14382
14582
|
init_types();
|
|
14383
14583
|
init_database();
|
|
@@ -14734,6 +14934,24 @@ function signPayload(body, secret) {
|
|
|
14734
14934
|
}
|
|
14735
14935
|
return `sha256=${Math.abs(hash).toString(16).padStart(16, "0")}`;
|
|
14736
14936
|
}
|
|
14937
|
+
function formatDiscordPayload(payload) {
|
|
14938
|
+
const isPassed = payload.run.status === "passed";
|
|
14939
|
+
const color = isPassed ? 2278750 : 15680580;
|
|
14940
|
+
return {
|
|
14941
|
+
username: "open-testers",
|
|
14942
|
+
embeds: [
|
|
14943
|
+
{
|
|
14944
|
+
title: `Test Run ${payload.run.status.toUpperCase()}`,
|
|
14945
|
+
color,
|
|
14946
|
+
description: `URL: ${payload.run.url}
|
|
14947
|
+
` + `Results: ${payload.run.passed}/${payload.run.total} passed` + (payload.run.failed > 0 ? ` (${payload.run.failed} failed)` : "") + (payload.schedule ? `
|
|
14948
|
+
Schedule: ${payload.schedule.name}` : ""),
|
|
14949
|
+
timestamp: payload.timestamp,
|
|
14950
|
+
footer: { text: "open-testers" }
|
|
14951
|
+
}
|
|
14952
|
+
]
|
|
14953
|
+
};
|
|
14954
|
+
}
|
|
14737
14955
|
function formatSlackPayload(payload) {
|
|
14738
14956
|
const status = payload.run.status === "passed" ? ":white_check_mark:" : ":x:";
|
|
14739
14957
|
const color = payload.run.status === "passed" ? "#22c55e" : "#ef4444";
|
|
@@ -14776,7 +14994,8 @@ async function dispatchWebhooks(event, run, schedule) {
|
|
|
14776
14994
|
if (!webhook.events.includes(event) && !webhook.events.includes("*"))
|
|
14777
14995
|
continue;
|
|
14778
14996
|
const isSlack = webhook.url.includes("hooks.slack.com");
|
|
14779
|
-
const
|
|
14997
|
+
const isDiscord = webhook.url.includes("discord.com/api/webhooks") || webhook.url.includes("discordapp.com/api/webhooks");
|
|
14998
|
+
const body = isSlack ? JSON.stringify(formatSlackPayload(payload)) : isDiscord ? JSON.stringify(formatDiscordPayload(payload)) : JSON.stringify(payload);
|
|
14780
14999
|
const headers = {
|
|
14781
15000
|
"Content-Type": "application/json"
|
|
14782
15001
|
};
|
|
@@ -15186,13 +15405,35 @@ async function runSingleScenario(scenario, runId, options) {
|
|
|
15186
15405
|
emit({ type: "scenario:start", scenarioId: scenario.id, scenarioName: scenario.name, resultId: result.id, runId });
|
|
15187
15406
|
let browser = null;
|
|
15188
15407
|
let page = null;
|
|
15408
|
+
let context = null;
|
|
15409
|
+
let harPath = null;
|
|
15189
15410
|
let stopNetworkLogging = null;
|
|
15190
15411
|
const networkErrors = [];
|
|
15191
15412
|
try {
|
|
15192
15413
|
browser = await launchBrowser({ headless: !(effectiveOptions.headed ?? false), engine: effectiveOptions.engine });
|
|
15193
|
-
|
|
15194
|
-
|
|
15195
|
-
|
|
15414
|
+
const useHar = effectiveOptions.engine !== "lightpanda" && effectiveOptions.engine !== "bun";
|
|
15415
|
+
if (useHar) {
|
|
15416
|
+
const testersDir = process.env["HASNA_TESTERS_DIR"] || join13(process.env["HOME"] || "", ".hasna", "testers");
|
|
15417
|
+
const harDir = join13(testersDir, "hars");
|
|
15418
|
+
mkdirSync8(harDir, { recursive: true });
|
|
15419
|
+
harPath = join13(harDir, `${result.id}.har`);
|
|
15420
|
+
const contextOptions = {
|
|
15421
|
+
viewport: config.browser.viewport,
|
|
15422
|
+
recordHar: { path: harPath, mode: "full" }
|
|
15423
|
+
};
|
|
15424
|
+
if (effectiveOptions.recordVideo) {
|
|
15425
|
+
const videoDir = join13(testersDir, "videos");
|
|
15426
|
+
mkdirSync8(videoDir, { recursive: true });
|
|
15427
|
+
contextOptions.recordVideo = { dir: videoDir, size: config.browser.viewport };
|
|
15428
|
+
}
|
|
15429
|
+
context = await browser.newContext(contextOptions);
|
|
15430
|
+
page = await context.newPage();
|
|
15431
|
+
} else {
|
|
15432
|
+
page = await getPage(browser, {
|
|
15433
|
+
viewport: config.browser.viewport,
|
|
15434
|
+
engine: effectiveOptions.engine
|
|
15435
|
+
});
|
|
15436
|
+
}
|
|
15196
15437
|
const targetUrl = scenario.targetPath ? `${options.url.replace(/\/$/, "")}${scenario.targetPath}` : options.url;
|
|
15197
15438
|
const scenarioTimeout = scenario.timeoutMs ?? options.timeout ?? config.browser.timeout ?? 60000;
|
|
15198
15439
|
registerSession({
|
|
@@ -15212,7 +15453,11 @@ async function runSingleScenario(scenario, runId, options) {
|
|
|
15212
15453
|
}
|
|
15213
15454
|
});
|
|
15214
15455
|
const consoleErrors = [];
|
|
15456
|
+
const consoleLogs = [];
|
|
15457
|
+
let currentStep = 0;
|
|
15215
15458
|
page.on("console", (msg) => {
|
|
15459
|
+
const logEntry = { step: currentStep > 0 ? currentStep : null, type: msg.type(), text: msg.text(), timestamp: Date.now() };
|
|
15460
|
+
consoleLogs.push(logEntry);
|
|
15216
15461
|
if (msg.type() === "error")
|
|
15217
15462
|
consoleErrors.push(msg.text());
|
|
15218
15463
|
});
|
|
@@ -15244,6 +15489,7 @@ async function runSingleScenario(scenario, runId, options) {
|
|
|
15244
15489
|
}
|
|
15245
15490
|
await page.goto(targetUrl, { timeout: Math.min(scenarioTimeout, 30000) });
|
|
15246
15491
|
const stepStartTimes = new Map;
|
|
15492
|
+
const stepResultIds = new Map;
|
|
15247
15493
|
const agentResult = await withTimeout(runAgentLoop({
|
|
15248
15494
|
client,
|
|
15249
15495
|
page,
|
|
@@ -15266,13 +15512,32 @@ async function runSingleScenario(scenario, runId, options) {
|
|
|
15266
15512
|
onStep: (stepEvent) => {
|
|
15267
15513
|
let stepDurationMs;
|
|
15268
15514
|
if (stepEvent.type === "tool_call") {
|
|
15515
|
+
currentStep = stepEvent.stepNumber;
|
|
15269
15516
|
stepStartTimes.set(stepEvent.stepNumber, Date.now());
|
|
15517
|
+
const stepResult = createStepResult({
|
|
15518
|
+
resultId: result.id,
|
|
15519
|
+
stepNumber: stepEvent.stepNumber,
|
|
15520
|
+
action: stepEvent.toolName ?? `step-${stepEvent.stepNumber}`,
|
|
15521
|
+
toolName: stepEvent.toolName,
|
|
15522
|
+
toolInput: stepEvent.toolInput,
|
|
15523
|
+
thinking: stepEvent.thinking
|
|
15524
|
+
});
|
|
15525
|
+
stepResultIds.set(stepEvent.stepNumber, stepResult.id);
|
|
15270
15526
|
} else if (stepEvent.type === "tool_result") {
|
|
15271
15527
|
const startTime = stepStartTimes.get(stepEvent.stepNumber);
|
|
15272
15528
|
if (startTime !== undefined) {
|
|
15273
15529
|
stepDurationMs = Date.now() - startTime;
|
|
15274
15530
|
stepStartTimes.delete(stepEvent.stepNumber);
|
|
15275
15531
|
}
|
|
15532
|
+
const stepResultId = stepResultIds.get(stepEvent.stepNumber);
|
|
15533
|
+
if (stepResultId) {
|
|
15534
|
+
const isSuccess = !stepEvent.toolResult?.toLowerCase().includes("error") && !stepEvent.toolResult?.toLowerCase().includes("failed");
|
|
15535
|
+
updateStepResult(stepResultId, {
|
|
15536
|
+
status: isSuccess ? "passed" : "failed",
|
|
15537
|
+
toolResult: stepEvent.toolResult,
|
|
15538
|
+
durationMs: stepDurationMs
|
|
15539
|
+
});
|
|
15540
|
+
}
|
|
15276
15541
|
}
|
|
15277
15542
|
emit({
|
|
15278
15543
|
type: `step:${stepEvent.type}`,
|
|
@@ -15321,7 +15586,7 @@ async function runSingleScenario(scenario, runId, options) {
|
|
|
15321
15586
|
durationMs: Date.now() - new Date(result.createdAt).getTime(),
|
|
15322
15587
|
tokensUsed: agentResult.tokensUsed,
|
|
15323
15588
|
costCents: estimateCost(model, agentResult.tokensUsed),
|
|
15324
|
-
metadata: networkErrors.length > 0 ? networkMeta :
|
|
15589
|
+
metadata: { consoleLogs, ...networkErrors.length > 0 ? networkMeta : {} }
|
|
15325
15590
|
});
|
|
15326
15591
|
if (agentResult.status === "failed" || agentResult.status === "error") {
|
|
15327
15592
|
const failureAnalysis = analyzeFailure(null, agentResult.reasoning ?? null);
|
|
@@ -15357,8 +15622,16 @@ async function runSingleScenario(scenario, runId, options) {
|
|
|
15357
15622
|
emit({ type: "scenario:error", scenarioId: scenario.id, scenarioName: scenario.name, error: errorMsg, runId });
|
|
15358
15623
|
return updatedResult;
|
|
15359
15624
|
} finally {
|
|
15360
|
-
if (
|
|
15361
|
-
|
|
15625
|
+
if (harPath) {
|
|
15626
|
+
try {
|
|
15627
|
+
updateResult(result.id, { metadata: { harPath } });
|
|
15628
|
+
} catch {}
|
|
15629
|
+
}
|
|
15630
|
+
if (browser) {
|
|
15631
|
+
try {
|
|
15632
|
+
await closeBrowser(browser, effectiveOptions.engine);
|
|
15633
|
+
} catch {}
|
|
15634
|
+
}
|
|
15362
15635
|
}
|
|
15363
15636
|
}
|
|
15364
15637
|
async function runBatch(scenarios, options) {
|
|
@@ -16111,10 +16384,10 @@ class Scheduler {
|
|
|
16111
16384
|
}
|
|
16112
16385
|
// src/lib/init.ts
|
|
16113
16386
|
init_paths();
|
|
16114
|
-
import { existsSync as existsSync11, readFileSync as readFileSync3, writeFileSync as writeFileSync3, mkdirSync as
|
|
16115
|
-
import { join as
|
|
16387
|
+
import { existsSync as existsSync11, readFileSync as readFileSync3, writeFileSync as writeFileSync3, mkdirSync as mkdirSync9 } from "fs";
|
|
16388
|
+
import { join as join14, basename } from "path";
|
|
16116
16389
|
function detectFramework(dir) {
|
|
16117
|
-
const pkgPath =
|
|
16390
|
+
const pkgPath = join14(dir, "package.json");
|
|
16118
16391
|
if (!existsSync11(pkgPath))
|
|
16119
16392
|
return null;
|
|
16120
16393
|
let pkg;
|
|
@@ -16342,9 +16615,9 @@ function initProject(options) {
|
|
|
16342
16615
|
}
|
|
16343
16616
|
}).filter((s) => s !== null);
|
|
16344
16617
|
const configDir = getTestersDir();
|
|
16345
|
-
const configPath =
|
|
16618
|
+
const configPath = join14(configDir, "config.json");
|
|
16346
16619
|
if (!existsSync11(configDir)) {
|
|
16347
|
-
|
|
16620
|
+
mkdirSync9(configDir, { recursive: true });
|
|
16348
16621
|
}
|
|
16349
16622
|
let config = {};
|
|
16350
16623
|
if (existsSync11(configPath)) {
|
|
@@ -16747,6 +17020,18 @@ var SCENARIO_TEMPLATES = {
|
|
|
16747
17020
|
a11y: [
|
|
16748
17021
|
{ name: "Keyboard navigation", description: "Navigate the page using only keyboard (Tab, Enter, Escape). Verify all interactive elements are reachable and focusable.", tags: ["a11y", "keyboard"], priority: "high", steps: ["Press Tab to move through elements", "Verify focus indicators are visible", "Press Enter on buttons/links", "Verify actions trigger correctly", "Press Escape to close modals/dropdowns"] },
|
|
16749
17022
|
{ name: "Image alt text", description: "Check that all images have meaningful alt text attributes.", tags: ["a11y"], priority: "medium", steps: ["Find all images on the page", "Check each image has an alt attribute", "Verify alt text is descriptive, not empty or generic"] }
|
|
17023
|
+
],
|
|
17024
|
+
checkout: [
|
|
17025
|
+
{ name: "Add item to cart", description: "Navigate to a product page, add an item to cart, and verify cart count increments.", tags: ["checkout", "cart"], priority: "critical", steps: ["Navigate to product detail page", "Click add to cart button", "Verify cart count badge updates", "Verify success notification appears"] },
|
|
17026
|
+
{ name: "Cart page shows added items", description: "Navigate to the cart page and verify previously added items are listed with correct prices and quantities.", tags: ["checkout", "cart"], priority: "high", steps: ["Navigate to cart page", "Verify all added items are listed", "Verify prices and quantities are correct", "Verify subtotal is calculated"] },
|
|
17027
|
+
{ name: "Checkout flow completion", description: "Complete the full checkout flow: cart -> shipping -> payment -> confirmation. Verify order is placed successfully.", tags: ["checkout", "smoke"], priority: "critical", steps: ["Navigate to cart", "Click proceed to checkout", "Fill shipping address", "Select shipping method", "Fill payment details", "Review order summary", "Place order", "Verify order confirmation page"] },
|
|
17028
|
+
{ name: "Apply coupon/discount code", description: "Enter a valid coupon code on the cart or checkout page and verify discount is applied to the total.", tags: ["checkout", "discount"], priority: "high", steps: ["Navigate to cart or checkout", "Enter coupon code", "Click apply", "Verify discount amount is shown", "Verify total is updated"] }
|
|
17029
|
+
],
|
|
17030
|
+
search: [
|
|
17031
|
+
{ name: "Search returns relevant results", description: "Enter a known search term and verify relevant results appear. Check that results match the query intent.", tags: ["search"], priority: "high", steps: ["Navigate to search page or focus search bar", "Enter known search term", "Submit search", "Verify results appear", "Verify result titles contain search term"] },
|
|
17032
|
+
{ name: "Empty search handling", description: "Submit an empty search and verify the system handles it gracefully (show all results or a prompt, no errors).", tags: ["search", "validation"], priority: "medium", steps: ["Focus search bar", "Submit empty search", "Verify no crash or error state", "Verify fallback behavior (all results or prompt)"] },
|
|
17033
|
+
{ name: "No results handling", description: "Search for a term that returns no results and verify appropriate 'no results found' messaging is displayed.", tags: ["search"], priority: "medium", steps: ["Navigate to search", "Enter nonsense term like 'xyzqwrtp'", "Submit search", "Verify no results found message", "Verify no error states"] },
|
|
17034
|
+
{ name: "Search filters work", description: "Perform a search, apply a filter (category, date, price range), and verify results are narrowed down.", tags: ["search", "filter"], priority: "high", steps: ["Perform initial search", "Note initial result count", "Apply a filter", "Verify result count decreased", "Verify results match filter criteria"] }
|
|
16750
17035
|
]
|
|
16751
17036
|
};
|
|
16752
17037
|
function getTemplate(name) {
|
|
@@ -17084,6 +17369,132 @@ async function startWatcher(options) {
|
|
|
17084
17369
|
process.on("SIGTERM", cleanup);
|
|
17085
17370
|
await new Promise(() => {});
|
|
17086
17371
|
}
|
|
17372
|
+
// src/lib/ci.ts
|
|
17373
|
+
function generateGitHubActionsWorkflow() {
|
|
17374
|
+
return `name: AI QA Tests
|
|
17375
|
+
on:
|
|
17376
|
+
pull_request:
|
|
17377
|
+
push:
|
|
17378
|
+
branches: [main]
|
|
17379
|
+
|
|
17380
|
+
permissions:
|
|
17381
|
+
contents: read
|
|
17382
|
+
pull-requests: write
|
|
17383
|
+
|
|
17384
|
+
jobs:
|
|
17385
|
+
test:
|
|
17386
|
+
runs-on: ubuntu-latest
|
|
17387
|
+
steps:
|
|
17388
|
+
- uses: actions/checkout@v4
|
|
17389
|
+
- uses: oven-sh/setup-bun@v2
|
|
17390
|
+
- run: bun install -g @hasna/testers
|
|
17391
|
+
- run: testers install-browser
|
|
17392
|
+
- run: testers run \${{ env.TEST_URL }} --json --output results.json --github-comment
|
|
17393
|
+
env:
|
|
17394
|
+
ANTHROPIC_API_KEY: \${{ secrets.ANTHROPIC_API_KEY }}
|
|
17395
|
+
TEST_URL: \${{ vars.TEST_URL || 'http://localhost:3000' }}
|
|
17396
|
+
GITHUB_TOKEN: \${{ secrets.GITHUB_TOKEN }}
|
|
17397
|
+
- run: testers report --latest --output report.html
|
|
17398
|
+
if: always()
|
|
17399
|
+
- uses: actions/upload-artifact@v4
|
|
17400
|
+
if: always()
|
|
17401
|
+
with:
|
|
17402
|
+
name: test-report
|
|
17403
|
+
path: |
|
|
17404
|
+
report.html
|
|
17405
|
+
results.json
|
|
17406
|
+
`;
|
|
17407
|
+
}
|
|
17408
|
+
function formatPRComment(run, results, dashboardUrl) {
|
|
17409
|
+
const icon = run.status === "passed" ? "\u2705" : run.status === "failed" ? "\u274C" : "\u26A0\uFE0F";
|
|
17410
|
+
const passRate = run.total > 0 ? Math.round(run.passed / run.total * 100) : 0;
|
|
17411
|
+
const ordered = [...results].sort((a, b) => {
|
|
17412
|
+
const rank = (s) => s === "failed" ? 0 : s === "error" ? 1 : s === "flaky" ? 2 : s === "skipped" ? 3 : 4;
|
|
17413
|
+
return rank(a.status) - rank(b.status);
|
|
17414
|
+
});
|
|
17415
|
+
const MAX_ROWS = 20;
|
|
17416
|
+
const rows = ordered.slice(0, MAX_ROWS).map((r) => {
|
|
17417
|
+
const rowIcon = r.status === "passed" ? "\u2705" : r.status === "failed" ? "\u274C" : r.status === "error" ? "\u26A0\uFE0F" : r.status === "flaky" ? "\uD83D\uDFE1" : "\u23ED\uFE0F";
|
|
17418
|
+
const dur = r.durationMs > 0 ? `${(r.durationMs / 1000).toFixed(1)}s` : "\u2014";
|
|
17419
|
+
const scenario = (() => {
|
|
17420
|
+
try {
|
|
17421
|
+
return getScenario(r.scenarioId);
|
|
17422
|
+
} catch {
|
|
17423
|
+
return null;
|
|
17424
|
+
}
|
|
17425
|
+
})();
|
|
17426
|
+
const name = scenario ? scenario.name : r.scenarioId.slice(0, 8);
|
|
17427
|
+
const safeName = name.replace(/\|/g, "\\|");
|
|
17428
|
+
const errSource = r.error ?? r.reasoning ?? "";
|
|
17429
|
+
const err = errSource ? ` ${errSource.replace(/\s+/g, " ").slice(0, 140).replace(/\|/g, "\\|")}` : "";
|
|
17430
|
+
return `| ${rowIcon} | ${safeName} | ${r.status} | ${dur} |${err} |`;
|
|
17431
|
+
}).join(`
|
|
17432
|
+
`);
|
|
17433
|
+
const truncated = results.length > MAX_ROWS ? `
|
|
17434
|
+
_...and ${results.length - MAX_ROWS} more_` : "";
|
|
17435
|
+
const dashLink = dashboardUrl ? `
|
|
17436
|
+
|
|
17437
|
+
[View full report \u2192](${dashboardUrl}/runs/${run.id})` : "";
|
|
17438
|
+
const totalCostCents = results.reduce((sum, r) => sum + (r.costCents ?? 0), 0);
|
|
17439
|
+
const costStr = totalCostCents > 0 ? ` \xB7 $${(totalCostCents / 100).toFixed(4)}` : "";
|
|
17440
|
+
const headerRow = `| | Scenario | Status | Duration | Details |
|
|
17441
|
+
|---|---|---|---|---|`;
|
|
17442
|
+
const body = results.length > 0 ? `${headerRow}
|
|
17443
|
+
${rows}${truncated}` : `_No scenarios ran. Use \`testers add\` to create scenarios or run with a URL to auto-generate them._`;
|
|
17444
|
+
return `## ${icon} AI QA Tests \u2014 ${run.status.toUpperCase()}
|
|
17445
|
+
|
|
17446
|
+
**${run.passed}/${run.total} passed** (${passRate}%) \xB7 URL: \`${run.url}\`${costStr}
|
|
17447
|
+
|
|
17448
|
+
${body}${dashLink}
|
|
17449
|
+
|
|
17450
|
+
_Generated by [@hasna/testers](https://www.npmjs.com/package/@hasna/testers)_`;
|
|
17451
|
+
}
|
|
17452
|
+
function resolvePullRequestNumber(explicit) {
|
|
17453
|
+
if (explicit && Number.isFinite(explicit))
|
|
17454
|
+
return explicit;
|
|
17455
|
+
const fromEnv = process.env["GITHUB_PR_NUMBER"];
|
|
17456
|
+
if (fromEnv) {
|
|
17457
|
+
const parsed = parseInt(fromEnv, 10);
|
|
17458
|
+
if (Number.isFinite(parsed))
|
|
17459
|
+
return parsed;
|
|
17460
|
+
}
|
|
17461
|
+
const ref = process.env["GITHUB_REF"] ?? "";
|
|
17462
|
+
const match = ref.match(/refs\/pull\/(\d+)\//);
|
|
17463
|
+
if (match && match[1]) {
|
|
17464
|
+
const parsed = parseInt(match[1], 10);
|
|
17465
|
+
if (Number.isFinite(parsed))
|
|
17466
|
+
return parsed;
|
|
17467
|
+
}
|
|
17468
|
+
return null;
|
|
17469
|
+
}
|
|
17470
|
+
async function postGitHubComment(run, results, options) {
|
|
17471
|
+
const token = process.env["GITHUB_TOKEN"];
|
|
17472
|
+
if (!token)
|
|
17473
|
+
return false;
|
|
17474
|
+
const prNumber = resolvePullRequestNumber(options?.prNumber);
|
|
17475
|
+
if (prNumber === null)
|
|
17476
|
+
return false;
|
|
17477
|
+
const repo = process.env["GITHUB_REPOSITORY"];
|
|
17478
|
+
if (!repo)
|
|
17479
|
+
return false;
|
|
17480
|
+
const body = formatPRComment(run, results, options?.dashboardUrl ?? process.env["TESTERS_DASHBOARD_URL"]);
|
|
17481
|
+
const apiUrl = `https://api.github.com/repos/${repo}/issues/${prNumber}/comments`;
|
|
17482
|
+
try {
|
|
17483
|
+
const response = await fetch(apiUrl, {
|
|
17484
|
+
method: "POST",
|
|
17485
|
+
headers: {
|
|
17486
|
+
Authorization: `Bearer ${token}`,
|
|
17487
|
+
"Content-Type": "application/json",
|
|
17488
|
+
Accept: "application/vnd.github+json",
|
|
17489
|
+
"X-GitHub-Api-Version": "2022-11-28"
|
|
17490
|
+
},
|
|
17491
|
+
body: JSON.stringify({ body })
|
|
17492
|
+
});
|
|
17493
|
+
return response.ok;
|
|
17494
|
+
} catch {
|
|
17495
|
+
return false;
|
|
17496
|
+
}
|
|
17497
|
+
}
|
|
17087
17498
|
export {
|
|
17088
17499
|
writeScenarioMeta,
|
|
17089
17500
|
writeRunMeta,
|
|
@@ -17111,6 +17522,7 @@ export {
|
|
|
17111
17522
|
runBatch,
|
|
17112
17523
|
runAgentLoop,
|
|
17113
17524
|
resultFromRow,
|
|
17525
|
+
resolvePullRequestNumber,
|
|
17114
17526
|
resolvePartialId,
|
|
17115
17527
|
resolveModel as resolveModelConfig,
|
|
17116
17528
|
resolveModel2 as resolveModel,
|
|
@@ -17120,6 +17532,7 @@ export {
|
|
|
17120
17532
|
registerAgent,
|
|
17121
17533
|
pullTasks,
|
|
17122
17534
|
projectFromRow,
|
|
17535
|
+
postGitHubComment,
|
|
17123
17536
|
parseSmokeIssues,
|
|
17124
17537
|
parseCronField,
|
|
17125
17538
|
parseCron,
|
|
@@ -17179,6 +17592,7 @@ export {
|
|
|
17179
17592
|
getAgent,
|
|
17180
17593
|
generateLatestReport,
|
|
17181
17594
|
generateHtmlReport,
|
|
17595
|
+
generateGitHubActionsWorkflow,
|
|
17182
17596
|
generateFilename,
|
|
17183
17597
|
formatTerminal,
|
|
17184
17598
|
formatSummary,
|
|
@@ -17186,6 +17600,7 @@ export {
|
|
|
17186
17600
|
formatScenarioList,
|
|
17187
17601
|
formatRunList,
|
|
17188
17602
|
formatResultDetail,
|
|
17603
|
+
formatPRComment,
|
|
17189
17604
|
formatJSON,
|
|
17190
17605
|
formatDiffTerminal,
|
|
17191
17606
|
formatDiffJSON,
|