@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/mcp/index.js
CHANGED
|
@@ -10095,7 +10095,8 @@ function scenarioFromRow(row) {
|
|
|
10095
10095
|
createdAt: row.created_at,
|
|
10096
10096
|
updatedAt: row.updated_at,
|
|
10097
10097
|
lastPassedAt: row.last_passed_at ?? null,
|
|
10098
|
-
lastPassedUrl: row.last_passed_url ?? null
|
|
10098
|
+
lastPassedUrl: row.last_passed_url ?? null,
|
|
10099
|
+
parameters: row.parameters ? JSON.parse(row.parameters) : null
|
|
10099
10100
|
};
|
|
10100
10101
|
}
|
|
10101
10102
|
function runFromRow(row) {
|
|
@@ -10115,7 +10116,14 @@ function runFromRow(row) {
|
|
|
10115
10116
|
metadata: row.metadata ? JSON.parse(row.metadata) : null,
|
|
10116
10117
|
isBaseline: row.is_baseline === 1,
|
|
10117
10118
|
samples: row.samples ?? 1,
|
|
10118
|
-
flakinessThreshold: row.flakiness_threshold ?? 0.95
|
|
10119
|
+
flakinessThreshold: row.flakiness_threshold ?? 0.95,
|
|
10120
|
+
prNumber: row.pr_number ?? null,
|
|
10121
|
+
prTitle: row.pr_title ?? null,
|
|
10122
|
+
prBranch: row.pr_branch ?? null,
|
|
10123
|
+
prBaseBranch: row.pr_base_branch ?? null,
|
|
10124
|
+
prCommitSha: row.pr_commit_sha ?? null,
|
|
10125
|
+
prUrl: row.pr_url ?? null,
|
|
10126
|
+
ghAppInstallationId: row.gh_app_installation_id ?? null
|
|
10119
10127
|
};
|
|
10120
10128
|
}
|
|
10121
10129
|
function resultFromRow(row) {
|
|
@@ -10136,7 +10144,8 @@ function resultFromRow(row) {
|
|
|
10136
10144
|
createdAt: row.created_at,
|
|
10137
10145
|
personaId: row.persona_id ?? null,
|
|
10138
10146
|
personaName: row.persona_name ?? null,
|
|
10139
|
-
failureAnalysis: row.failure_analysis ? JSON.parse(row.failure_analysis) : null
|
|
10147
|
+
failureAnalysis: row.failure_analysis ? JSON.parse(row.failure_analysis) : null,
|
|
10148
|
+
harPath: row.har_path ?? null
|
|
10140
10149
|
};
|
|
10141
10150
|
}
|
|
10142
10151
|
function screenshotFromRow(row) {
|
|
@@ -10228,7 +10237,10 @@ function personaFromRow(row) {
|
|
|
10228
10237
|
email: row.auth_email,
|
|
10229
10238
|
password: row.auth_password,
|
|
10230
10239
|
loginPath: row.auth_login_path ?? "/login",
|
|
10231
|
-
cookies: row.auth_cookies ? JSON.parse(row.auth_cookies) : null
|
|
10240
|
+
cookies: row.auth_cookies ? JSON.parse(row.auth_cookies) : null,
|
|
10241
|
+
strategy: row.auth_strategy ?? "form-login",
|
|
10242
|
+
headers: row.auth_headers ? JSON.parse(row.auth_headers) : undefined,
|
|
10243
|
+
customScript: row.auth_script ?? undefined
|
|
10232
10244
|
} : null
|
|
10233
10245
|
};
|
|
10234
10246
|
}
|
|
@@ -10776,6 +10788,43 @@ ALTER TABLE scenarios ADD COLUMN required_role TEXT;
|
|
|
10776
10788
|
machine_id TEXT,
|
|
10777
10789
|
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
10778
10790
|
);
|
|
10791
|
+
`,
|
|
10792
|
+
`
|
|
10793
|
+
ALTER TABLE results ADD COLUMN har_path TEXT;
|
|
10794
|
+
`,
|
|
10795
|
+
`
|
|
10796
|
+
ALTER TABLE scenarios ADD COLUMN parameters TEXT;
|
|
10797
|
+
`,
|
|
10798
|
+
`
|
|
10799
|
+
ALTER TABLE personas ADD COLUMN auth_strategy TEXT DEFAULT 'form-login';
|
|
10800
|
+
ALTER TABLE personas ADD COLUMN auth_headers TEXT;
|
|
10801
|
+
ALTER TABLE personas ADD COLUMN auth_script TEXT;
|
|
10802
|
+
`,
|
|
10803
|
+
`
|
|
10804
|
+
CREATE TABLE IF NOT EXISTS step_results (
|
|
10805
|
+
id TEXT PRIMARY KEY,
|
|
10806
|
+
result_id TEXT NOT NULL REFERENCES results(id) ON DELETE CASCADE,
|
|
10807
|
+
step_number INTEGER NOT NULL,
|
|
10808
|
+
action TEXT NOT NULL,
|
|
10809
|
+
status TEXT NOT NULL DEFAULT 'running' CHECK(status IN ('passed','failed','error','running','skipped')),
|
|
10810
|
+
tool_name TEXT,
|
|
10811
|
+
tool_input TEXT,
|
|
10812
|
+
tool_result TEXT,
|
|
10813
|
+
thinking TEXT,
|
|
10814
|
+
error TEXT,
|
|
10815
|
+
duration_ms INTEGER,
|
|
10816
|
+
screenshot_id TEXT REFERENCES screenshots(id),
|
|
10817
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
10818
|
+
);
|
|
10819
|
+
`,
|
|
10820
|
+
`
|
|
10821
|
+
ALTER TABLE runs ADD COLUMN pr_number INTEGER;
|
|
10822
|
+
ALTER TABLE runs ADD COLUMN pr_title TEXT;
|
|
10823
|
+
ALTER TABLE runs ADD COLUMN pr_branch TEXT;
|
|
10824
|
+
ALTER TABLE runs ADD COLUMN pr_base_branch TEXT;
|
|
10825
|
+
ALTER TABLE runs ADD COLUMN pr_commit_sha TEXT;
|
|
10826
|
+
ALTER TABLE runs ADD COLUMN pr_url TEXT;
|
|
10827
|
+
ALTER TABLE runs ADD COLUMN gh_app_installation_id TEXT;
|
|
10779
10828
|
`
|
|
10780
10829
|
];
|
|
10781
10830
|
});
|
|
@@ -10799,9 +10848,9 @@ function createScenario(input) {
|
|
|
10799
10848
|
const short_id = nextShortId(input.projectId);
|
|
10800
10849
|
const timestamp = now();
|
|
10801
10850
|
db2.query(`
|
|
10802
|
-
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)
|
|
10803
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?, ?)
|
|
10804
|
-
`).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);
|
|
10851
|
+
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)
|
|
10852
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?, ?)
|
|
10853
|
+
`).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);
|
|
10805
10854
|
return getScenario(id);
|
|
10806
10855
|
}
|
|
10807
10856
|
function getScenario(id) {
|
|
@@ -10951,6 +11000,10 @@ function updateScenario(id, input, version) {
|
|
|
10951
11000
|
sets.push("assertions = ?");
|
|
10952
11001
|
params.push(JSON.stringify(input.assertions));
|
|
10953
11002
|
}
|
|
11003
|
+
if (input.parameters !== undefined) {
|
|
11004
|
+
sets.push("parameters = ?");
|
|
11005
|
+
params.push(JSON.stringify(input.parameters));
|
|
11006
|
+
}
|
|
10954
11007
|
if (sets.length === 0) {
|
|
10955
11008
|
return existing;
|
|
10956
11009
|
}
|
|
@@ -11005,9 +11058,9 @@ function createRun(input) {
|
|
|
11005
11058
|
const id = uuid();
|
|
11006
11059
|
const timestamp = now();
|
|
11007
11060
|
db2.query(`
|
|
11008
|
-
INSERT INTO runs (id, project_id, status, url, model, headed, parallel, total, passed, failed, started_at, finished_at, metadata, samples, flakiness_threshold)
|
|
11009
|
-
VALUES (?, ?, 'pending', ?, ?, ?, ?, 0, 0, 0, ?, NULL, ?, ?, ?)
|
|
11010
|
-
`).run(id, input.projectId ?? null, input.url, input.model, input.headed ? 1 : 0, input.parallel ?? 1, timestamp,
|
|
11061
|
+
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)
|
|
11062
|
+
VALUES (?, ?, 'pending', ?, ?, ?, ?, 0, 0, 0, ?, NULL, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
11063
|
+
`).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);
|
|
11011
11064
|
return getRun(id);
|
|
11012
11065
|
}
|
|
11013
11066
|
function getRun(id) {
|
|
@@ -11035,6 +11088,14 @@ function listRuns(filter) {
|
|
|
11035
11088
|
conditions.push("status = ?");
|
|
11036
11089
|
params.push(filter.status);
|
|
11037
11090
|
}
|
|
11091
|
+
if (filter?.since) {
|
|
11092
|
+
conditions.push("started_at >= ?");
|
|
11093
|
+
params.push(filter.since);
|
|
11094
|
+
}
|
|
11095
|
+
if (filter?.until) {
|
|
11096
|
+
conditions.push("started_at <= ?");
|
|
11097
|
+
params.push(filter.until);
|
|
11098
|
+
}
|
|
11038
11099
|
let sql = "SELECT * FROM runs";
|
|
11039
11100
|
if (conditions.length > 0) {
|
|
11040
11101
|
sql += " WHERE " + conditions.join(" AND ");
|
|
@@ -11123,6 +11184,15 @@ var init_runs = __esm(() => {
|
|
|
11123
11184
|
});
|
|
11124
11185
|
|
|
11125
11186
|
// src/db/results.ts
|
|
11187
|
+
var exports_results = {};
|
|
11188
|
+
__export(exports_results, {
|
|
11189
|
+
updateResult: () => updateResult,
|
|
11190
|
+
listResults: () => listResults,
|
|
11191
|
+
getResultsByRun: () => getResultsByRun,
|
|
11192
|
+
getResult: () => getResult,
|
|
11193
|
+
createResult: () => createResult,
|
|
11194
|
+
countResultsByRun: () => countResultsByRun
|
|
11195
|
+
});
|
|
11126
11196
|
function createResult(input) {
|
|
11127
11197
|
const db2 = getDatabase();
|
|
11128
11198
|
const id = uuid();
|
|
@@ -11205,6 +11275,11 @@ function updateResult(id, updates) {
|
|
|
11205
11275
|
function getResultsByRun(runId) {
|
|
11206
11276
|
return listResults(runId);
|
|
11207
11277
|
}
|
|
11278
|
+
function countResultsByRun(runId) {
|
|
11279
|
+
const db2 = getDatabase();
|
|
11280
|
+
const row = db2.query("SELECT COUNT(*) as count FROM results WHERE run_id = ?").get(runId);
|
|
11281
|
+
return row.count;
|
|
11282
|
+
}
|
|
11208
11283
|
var init_results = __esm(() => {
|
|
11209
11284
|
init_types2();
|
|
11210
11285
|
init_database();
|
|
@@ -11882,6 +11957,16 @@ async function launchBrowser(options) {
|
|
|
11882
11957
|
const headless = options?.headless ?? true;
|
|
11883
11958
|
const viewport = options?.viewport ?? DEFAULT_VIEWPORT;
|
|
11884
11959
|
try {
|
|
11960
|
+
if (engine === "playwright-firefox") {
|
|
11961
|
+
const { firefox } = await import("playwright");
|
|
11962
|
+
const browser = await firefox.launch({ headless });
|
|
11963
|
+
return browser;
|
|
11964
|
+
}
|
|
11965
|
+
if (engine === "playwright-webkit") {
|
|
11966
|
+
const { webkit } = await import("playwright");
|
|
11967
|
+
const browser = await webkit.launch({ headless });
|
|
11968
|
+
return browser;
|
|
11969
|
+
}
|
|
11885
11970
|
return await launchPlaywright({ headless, viewport });
|
|
11886
11971
|
} catch (error) {
|
|
11887
11972
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -11998,8 +12083,9 @@ async function installBrowser(engine) {
|
|
|
11998
12083
|
const { installLightpanda: installLightpanda2 } = await Promise.resolve().then(() => (init_browser_lightpanda(), exports_browser_lightpanda));
|
|
11999
12084
|
return installLightpanda2();
|
|
12000
12085
|
}
|
|
12086
|
+
const browserName = engine === "playwright-firefox" ? "firefox" : engine === "playwright-webkit" ? "webkit" : "chromium";
|
|
12001
12087
|
try {
|
|
12002
|
-
execSync(
|
|
12088
|
+
execSync(`bunx playwright install ${browserName}`, {
|
|
12003
12089
|
stdio: "inherit"
|
|
12004
12090
|
});
|
|
12005
12091
|
} catch (error) {
|
|
@@ -12137,7 +12223,7 @@ function getDefaultConfig() {
|
|
|
12137
12223
|
browser: {
|
|
12138
12224
|
headless: true,
|
|
12139
12225
|
viewport: { width: 1280, height: 720 },
|
|
12140
|
-
timeout:
|
|
12226
|
+
timeout: 120000
|
|
12141
12227
|
},
|
|
12142
12228
|
screenshots: {
|
|
12143
12229
|
dir: join9(getTestersDir(), "screenshots"),
|
|
@@ -14551,6 +14637,76 @@ var init_costs = __esm(() => {
|
|
|
14551
14637
|
};
|
|
14552
14638
|
});
|
|
14553
14639
|
|
|
14640
|
+
// src/db/step-results.ts
|
|
14641
|
+
function createStepResult(input) {
|
|
14642
|
+
const db2 = getDatabase();
|
|
14643
|
+
const id = uuid();
|
|
14644
|
+
const timestamp = now();
|
|
14645
|
+
db2.query(`
|
|
14646
|
+
INSERT INTO step_results (id, result_id, step_number, action, status, tool_name, tool_input, thinking, created_at)
|
|
14647
|
+
VALUES (?, ?, ?, ?, 'running', ?, ?, ?, ?)
|
|
14648
|
+
`).run(id, input.resultId, input.stepNumber, input.action, input.toolName ?? null, input.toolInput ? JSON.stringify(input.toolInput) : null, input.thinking ?? null, timestamp);
|
|
14649
|
+
return getStepResult(id);
|
|
14650
|
+
}
|
|
14651
|
+
function getStepResult(id) {
|
|
14652
|
+
const db2 = getDatabase();
|
|
14653
|
+
const row = db2.query("SELECT * FROM step_results WHERE id = ?").get(id);
|
|
14654
|
+
return row ? stepResultFromRow(row) : null;
|
|
14655
|
+
}
|
|
14656
|
+
function updateStepResult(id, updates) {
|
|
14657
|
+
const db2 = getDatabase();
|
|
14658
|
+
const existing = getStepResult(id);
|
|
14659
|
+
if (!existing)
|
|
14660
|
+
return null;
|
|
14661
|
+
const sets = [];
|
|
14662
|
+
const params = [];
|
|
14663
|
+
if (updates.status !== undefined) {
|
|
14664
|
+
sets.push("status = ?");
|
|
14665
|
+
params.push(updates.status);
|
|
14666
|
+
}
|
|
14667
|
+
if (updates.toolResult !== undefined) {
|
|
14668
|
+
sets.push("tool_result = ?");
|
|
14669
|
+
params.push(updates.toolResult);
|
|
14670
|
+
}
|
|
14671
|
+
if (updates.error !== undefined) {
|
|
14672
|
+
sets.push("error = ?");
|
|
14673
|
+
params.push(updates.error);
|
|
14674
|
+
}
|
|
14675
|
+
if (updates.durationMs !== undefined) {
|
|
14676
|
+
sets.push("duration_ms = ?");
|
|
14677
|
+
params.push(updates.durationMs);
|
|
14678
|
+
}
|
|
14679
|
+
if (updates.screenshotId !== undefined) {
|
|
14680
|
+
sets.push("screenshot_id = ?");
|
|
14681
|
+
params.push(updates.screenshotId);
|
|
14682
|
+
}
|
|
14683
|
+
if (sets.length === 0)
|
|
14684
|
+
return existing;
|
|
14685
|
+
params.push(id);
|
|
14686
|
+
db2.query(`UPDATE step_results SET ${sets.join(", ")} WHERE id = ?`).run(...params);
|
|
14687
|
+
return getStepResult(id);
|
|
14688
|
+
}
|
|
14689
|
+
function stepResultFromRow(row) {
|
|
14690
|
+
return {
|
|
14691
|
+
id: row.id,
|
|
14692
|
+
resultId: row.result_id,
|
|
14693
|
+
stepNumber: row.step_number,
|
|
14694
|
+
action: row.action,
|
|
14695
|
+
status: row.status,
|
|
14696
|
+
toolName: row.tool_name,
|
|
14697
|
+
toolInput: row.tool_input ? JSON.parse(row.tool_input) : null,
|
|
14698
|
+
toolResult: row.tool_result,
|
|
14699
|
+
thinking: row.thinking,
|
|
14700
|
+
error: row.error,
|
|
14701
|
+
durationMs: row.duration_ms,
|
|
14702
|
+
screenshotId: row.screenshot_id,
|
|
14703
|
+
createdAt: row.created_at
|
|
14704
|
+
};
|
|
14705
|
+
}
|
|
14706
|
+
var init_step_results = __esm(() => {
|
|
14707
|
+
init_database();
|
|
14708
|
+
});
|
|
14709
|
+
|
|
14554
14710
|
// src/db/personas.ts
|
|
14555
14711
|
function createPersona(input) {
|
|
14556
14712
|
const db2 = getDatabase();
|
|
@@ -14558,9 +14714,9 @@ function createPersona(input) {
|
|
|
14558
14714
|
const short_id = shortUuid();
|
|
14559
14715
|
const timestamp = now();
|
|
14560
14716
|
db2.query(`
|
|
14561
|
-
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)
|
|
14562
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?, ?)
|
|
14563
|
-
`).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);
|
|
14717
|
+
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)
|
|
14718
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?, ?)
|
|
14719
|
+
`).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);
|
|
14564
14720
|
return getPersona(id);
|
|
14565
14721
|
}
|
|
14566
14722
|
function getPersona(id) {
|
|
@@ -14678,6 +14834,18 @@ function updatePersona(id, updates, version) {
|
|
|
14678
14834
|
sets.push("auth_cookies = ?");
|
|
14679
14835
|
params.push(updates.authCookies ? JSON.stringify(updates.authCookies) : null);
|
|
14680
14836
|
}
|
|
14837
|
+
if (updates.authStrategy !== undefined) {
|
|
14838
|
+
sets.push("auth_strategy = ?");
|
|
14839
|
+
params.push(updates.authStrategy);
|
|
14840
|
+
}
|
|
14841
|
+
if (updates.authHeaders !== undefined) {
|
|
14842
|
+
sets.push("auth_headers = ?");
|
|
14843
|
+
params.push(JSON.stringify(updates.authHeaders));
|
|
14844
|
+
}
|
|
14845
|
+
if (updates.authCustomScript !== undefined) {
|
|
14846
|
+
sets.push("auth_script = ?");
|
|
14847
|
+
params.push(updates.authCustomScript);
|
|
14848
|
+
}
|
|
14681
14849
|
if (sets.length === 0) {
|
|
14682
14850
|
return existing;
|
|
14683
14851
|
}
|
|
@@ -15199,6 +15367,24 @@ function signPayload(body, secret) {
|
|
|
15199
15367
|
}
|
|
15200
15368
|
return `sha256=${Math.abs(hash).toString(16).padStart(16, "0")}`;
|
|
15201
15369
|
}
|
|
15370
|
+
function formatDiscordPayload(payload) {
|
|
15371
|
+
const isPassed = payload.run.status === "passed";
|
|
15372
|
+
const color = isPassed ? 2278750 : 15680580;
|
|
15373
|
+
return {
|
|
15374
|
+
username: "open-testers",
|
|
15375
|
+
embeds: [
|
|
15376
|
+
{
|
|
15377
|
+
title: `Test Run ${payload.run.status.toUpperCase()}`,
|
|
15378
|
+
color,
|
|
15379
|
+
description: `URL: ${payload.run.url}
|
|
15380
|
+
` + `Results: ${payload.run.passed}/${payload.run.total} passed` + (payload.run.failed > 0 ? ` (${payload.run.failed} failed)` : "") + (payload.schedule ? `
|
|
15381
|
+
Schedule: ${payload.schedule.name}` : ""),
|
|
15382
|
+
timestamp: payload.timestamp,
|
|
15383
|
+
footer: { text: "open-testers" }
|
|
15384
|
+
}
|
|
15385
|
+
]
|
|
15386
|
+
};
|
|
15387
|
+
}
|
|
15202
15388
|
function formatSlackPayload(payload) {
|
|
15203
15389
|
const status = payload.run.status === "passed" ? ":white_check_mark:" : ":x:";
|
|
15204
15390
|
const color = payload.run.status === "passed" ? "#22c55e" : "#ef4444";
|
|
@@ -15241,7 +15427,8 @@ async function dispatchWebhooks(event, run, schedule) {
|
|
|
15241
15427
|
if (!webhook.events.includes(event) && !webhook.events.includes("*"))
|
|
15242
15428
|
continue;
|
|
15243
15429
|
const isSlack = webhook.url.includes("hooks.slack.com");
|
|
15244
|
-
const
|
|
15430
|
+
const isDiscord = webhook.url.includes("discord.com/api/webhooks") || webhook.url.includes("discordapp.com/api/webhooks");
|
|
15431
|
+
const body = isSlack ? JSON.stringify(formatSlackPayload(payload)) : isDiscord ? JSON.stringify(formatDiscordPayload(payload)) : JSON.stringify(payload);
|
|
15245
15432
|
const headers = {
|
|
15246
15433
|
"Content-Type": "application/json"
|
|
15247
15434
|
};
|
|
@@ -15751,6 +15938,8 @@ __export(exports_runner, {
|
|
|
15751
15938
|
runBatch: () => runBatch,
|
|
15752
15939
|
onRunEvent: () => onRunEvent
|
|
15753
15940
|
});
|
|
15941
|
+
import { mkdirSync as mkdirSync8 } from "fs";
|
|
15942
|
+
import { join as join13 } from "path";
|
|
15754
15943
|
import { enableNetworkLogging } from "@hasna/browser";
|
|
15755
15944
|
function onRunEvent(handler) {
|
|
15756
15945
|
eventHandler = handler;
|
|
@@ -15836,13 +16025,35 @@ async function runSingleScenario(scenario, runId, options) {
|
|
|
15836
16025
|
emit({ type: "scenario:start", scenarioId: scenario.id, scenarioName: scenario.name, resultId: result.id, runId });
|
|
15837
16026
|
let browser = null;
|
|
15838
16027
|
let page = null;
|
|
16028
|
+
let context = null;
|
|
16029
|
+
let harPath = null;
|
|
15839
16030
|
let stopNetworkLogging = null;
|
|
15840
16031
|
const networkErrors = [];
|
|
15841
16032
|
try {
|
|
15842
16033
|
browser = await launchBrowser({ headless: !(effectiveOptions.headed ?? false), engine: effectiveOptions.engine });
|
|
15843
|
-
|
|
15844
|
-
|
|
15845
|
-
|
|
16034
|
+
const useHar = effectiveOptions.engine !== "lightpanda" && effectiveOptions.engine !== "bun";
|
|
16035
|
+
if (useHar) {
|
|
16036
|
+
const testersDir = process.env["HASNA_TESTERS_DIR"] || join13(process.env["HOME"] || "", ".hasna", "testers");
|
|
16037
|
+
const harDir = join13(testersDir, "hars");
|
|
16038
|
+
mkdirSync8(harDir, { recursive: true });
|
|
16039
|
+
harPath = join13(harDir, `${result.id}.har`);
|
|
16040
|
+
const contextOptions = {
|
|
16041
|
+
viewport: config.browser.viewport,
|
|
16042
|
+
recordHar: { path: harPath, mode: "full" }
|
|
16043
|
+
};
|
|
16044
|
+
if (effectiveOptions.recordVideo) {
|
|
16045
|
+
const videoDir = join13(testersDir, "videos");
|
|
16046
|
+
mkdirSync8(videoDir, { recursive: true });
|
|
16047
|
+
contextOptions.recordVideo = { dir: videoDir, size: config.browser.viewport };
|
|
16048
|
+
}
|
|
16049
|
+
context = await browser.newContext(contextOptions);
|
|
16050
|
+
page = await context.newPage();
|
|
16051
|
+
} else {
|
|
16052
|
+
page = await getPage(browser, {
|
|
16053
|
+
viewport: config.browser.viewport,
|
|
16054
|
+
engine: effectiveOptions.engine
|
|
16055
|
+
});
|
|
16056
|
+
}
|
|
15846
16057
|
const targetUrl = scenario.targetPath ? `${options.url.replace(/\/$/, "")}${scenario.targetPath}` : options.url;
|
|
15847
16058
|
const scenarioTimeout = scenario.timeoutMs ?? options.timeout ?? config.browser.timeout ?? 60000;
|
|
15848
16059
|
registerSession({
|
|
@@ -15862,7 +16073,11 @@ async function runSingleScenario(scenario, runId, options) {
|
|
|
15862
16073
|
}
|
|
15863
16074
|
});
|
|
15864
16075
|
const consoleErrors = [];
|
|
16076
|
+
const consoleLogs = [];
|
|
16077
|
+
let currentStep = 0;
|
|
15865
16078
|
page.on("console", (msg) => {
|
|
16079
|
+
const logEntry = { step: currentStep > 0 ? currentStep : null, type: msg.type(), text: msg.text(), timestamp: Date.now() };
|
|
16080
|
+
consoleLogs.push(logEntry);
|
|
15866
16081
|
if (msg.type() === "error")
|
|
15867
16082
|
consoleErrors.push(msg.text());
|
|
15868
16083
|
});
|
|
@@ -15894,6 +16109,7 @@ async function runSingleScenario(scenario, runId, options) {
|
|
|
15894
16109
|
}
|
|
15895
16110
|
await page.goto(targetUrl, { timeout: Math.min(scenarioTimeout, 30000) });
|
|
15896
16111
|
const stepStartTimes = new Map;
|
|
16112
|
+
const stepResultIds = new Map;
|
|
15897
16113
|
const agentResult = await withTimeout(runAgentLoop({
|
|
15898
16114
|
client,
|
|
15899
16115
|
page,
|
|
@@ -15916,13 +16132,32 @@ async function runSingleScenario(scenario, runId, options) {
|
|
|
15916
16132
|
onStep: (stepEvent) => {
|
|
15917
16133
|
let stepDurationMs;
|
|
15918
16134
|
if (stepEvent.type === "tool_call") {
|
|
16135
|
+
currentStep = stepEvent.stepNumber;
|
|
15919
16136
|
stepStartTimes.set(stepEvent.stepNumber, Date.now());
|
|
16137
|
+
const stepResult = createStepResult({
|
|
16138
|
+
resultId: result.id,
|
|
16139
|
+
stepNumber: stepEvent.stepNumber,
|
|
16140
|
+
action: stepEvent.toolName ?? `step-${stepEvent.stepNumber}`,
|
|
16141
|
+
toolName: stepEvent.toolName,
|
|
16142
|
+
toolInput: stepEvent.toolInput,
|
|
16143
|
+
thinking: stepEvent.thinking
|
|
16144
|
+
});
|
|
16145
|
+
stepResultIds.set(stepEvent.stepNumber, stepResult.id);
|
|
15920
16146
|
} else if (stepEvent.type === "tool_result") {
|
|
15921
16147
|
const startTime = stepStartTimes.get(stepEvent.stepNumber);
|
|
15922
16148
|
if (startTime !== undefined) {
|
|
15923
16149
|
stepDurationMs = Date.now() - startTime;
|
|
15924
16150
|
stepStartTimes.delete(stepEvent.stepNumber);
|
|
15925
16151
|
}
|
|
16152
|
+
const stepResultId = stepResultIds.get(stepEvent.stepNumber);
|
|
16153
|
+
if (stepResultId) {
|
|
16154
|
+
const isSuccess = !stepEvent.toolResult?.toLowerCase().includes("error") && !stepEvent.toolResult?.toLowerCase().includes("failed");
|
|
16155
|
+
updateStepResult(stepResultId, {
|
|
16156
|
+
status: isSuccess ? "passed" : "failed",
|
|
16157
|
+
toolResult: stepEvent.toolResult,
|
|
16158
|
+
durationMs: stepDurationMs
|
|
16159
|
+
});
|
|
16160
|
+
}
|
|
15926
16161
|
}
|
|
15927
16162
|
emit({
|
|
15928
16163
|
type: `step:${stepEvent.type}`,
|
|
@@ -15971,7 +16206,7 @@ async function runSingleScenario(scenario, runId, options) {
|
|
|
15971
16206
|
durationMs: Date.now() - new Date(result.createdAt).getTime(),
|
|
15972
16207
|
tokensUsed: agentResult.tokensUsed,
|
|
15973
16208
|
costCents: estimateCost(model, agentResult.tokensUsed),
|
|
15974
|
-
metadata: networkErrors.length > 0 ? networkMeta :
|
|
16209
|
+
metadata: { consoleLogs, ...networkErrors.length > 0 ? networkMeta : {} }
|
|
15975
16210
|
});
|
|
15976
16211
|
if (agentResult.status === "failed" || agentResult.status === "error") {
|
|
15977
16212
|
const failureAnalysis = analyzeFailure(null, agentResult.reasoning ?? null);
|
|
@@ -16007,8 +16242,16 @@ async function runSingleScenario(scenario, runId, options) {
|
|
|
16007
16242
|
emit({ type: "scenario:error", scenarioId: scenario.id, scenarioName: scenario.name, error: errorMsg, runId });
|
|
16008
16243
|
return updatedResult;
|
|
16009
16244
|
} finally {
|
|
16010
|
-
if (
|
|
16011
|
-
|
|
16245
|
+
if (harPath) {
|
|
16246
|
+
try {
|
|
16247
|
+
updateResult(result.id, { metadata: { harPath } });
|
|
16248
|
+
} catch {}
|
|
16249
|
+
}
|
|
16250
|
+
if (browser) {
|
|
16251
|
+
try {
|
|
16252
|
+
await closeBrowser(browser, effectiveOptions.engine);
|
|
16253
|
+
} catch {}
|
|
16254
|
+
}
|
|
16012
16255
|
}
|
|
16013
16256
|
}
|
|
16014
16257
|
async function runBatch(scenarios, options) {
|
|
@@ -16304,6 +16547,7 @@ var init_runner = __esm(() => {
|
|
|
16304
16547
|
init_results();
|
|
16305
16548
|
init_costs();
|
|
16306
16549
|
init_screenshots();
|
|
16550
|
+
init_step_results();
|
|
16307
16551
|
init_scenarios();
|
|
16308
16552
|
init_personas();
|
|
16309
16553
|
init_browser();
|
|
@@ -17341,6 +17585,16 @@ async function runHealthScan(options) {
|
|
|
17341
17585
|
});
|
|
17342
17586
|
results.push(piiResult);
|
|
17343
17587
|
}
|
|
17588
|
+
if (scanners.includes("a11y")) {
|
|
17589
|
+
const a11yResult = await scanA11y({
|
|
17590
|
+
url,
|
|
17591
|
+
pages,
|
|
17592
|
+
wcagLevel: options.wcagLevel ?? "AA",
|
|
17593
|
+
headed,
|
|
17594
|
+
timeoutMs
|
|
17595
|
+
});
|
|
17596
|
+
results.push(a11yResult);
|
|
17597
|
+
}
|
|
17344
17598
|
const allIssues = results.flatMap((r) => r.issues);
|
|
17345
17599
|
let newCount = 0;
|
|
17346
17600
|
let regressedCount = 0;
|
|
@@ -17556,10 +17810,10 @@ __export(exports_contacts_connector, {
|
|
|
17556
17810
|
function getContactsDb() {
|
|
17557
17811
|
const { Database: Database4 } = __require("bun:sqlite");
|
|
17558
17812
|
const { existsSync: existsSync5 } = __require("fs");
|
|
17559
|
-
const { join:
|
|
17813
|
+
const { join: join14 } = __require("path");
|
|
17560
17814
|
const { homedir: homedir7 } = __require("os");
|
|
17561
17815
|
const envPath = process.env["HASNA_CONTACTS_DB_PATH"] ?? process.env["OPEN_CONTACTS_DB"];
|
|
17562
|
-
const dbPath = envPath ??
|
|
17816
|
+
const dbPath = envPath ?? join14(homedir7(), ".hasna", "contacts", "contacts.db");
|
|
17563
17817
|
if (!existsSync5(dbPath))
|
|
17564
17818
|
return null;
|
|
17565
17819
|
const db2 = new Database4(dbPath, { readonly: true });
|
|
@@ -17680,7 +17934,7 @@ __export(exports_army_runner, {
|
|
|
17680
17934
|
waitForArmyRun: () => waitForArmyRun,
|
|
17681
17935
|
runWithArmy: () => runWithArmy
|
|
17682
17936
|
});
|
|
17683
|
-
import { join as
|
|
17937
|
+
import { join as join14 } from "path";
|
|
17684
17938
|
function chunkArray(arr, n) {
|
|
17685
17939
|
const chunks = [];
|
|
17686
17940
|
const size = Math.ceil(arr.length / n);
|
|
@@ -17690,7 +17944,7 @@ function chunkArray(arr, n) {
|
|
|
17690
17944
|
return chunks;
|
|
17691
17945
|
}
|
|
17692
17946
|
function getCliPath() {
|
|
17693
|
-
const srcPath =
|
|
17947
|
+
const srcPath = join14(import.meta.dir, "../cli/index.tsx");
|
|
17694
17948
|
return srcPath;
|
|
17695
17949
|
}
|
|
17696
17950
|
async function runWithArmy(options) {
|
|
@@ -22002,6 +22256,54 @@ var NEVER = INVALID;
|
|
|
22002
22256
|
// src/mcp/index.ts
|
|
22003
22257
|
init_dist();
|
|
22004
22258
|
init_scenarios();
|
|
22259
|
+
|
|
22260
|
+
// src/lib/templates.ts
|
|
22261
|
+
var SCENARIO_TEMPLATES = {
|
|
22262
|
+
auth: [
|
|
22263
|
+
{ name: "Login with valid credentials", description: "Navigate to the login page, enter valid credentials, submit the form, and verify redirect to authenticated area. Check that user menu/avatar is visible.", tags: ["auth", "smoke"], priority: "critical", requiresAuth: false, steps: ["Navigate to login page", "Enter email and password", "Submit login form", "Verify redirect to dashboard/home", "Verify user menu or avatar is visible"] },
|
|
22264
|
+
{ name: "Signup flow", description: "Navigate to signup page, fill all required fields with valid data, submit, and verify account creation succeeds.", tags: ["auth"], priority: "high", steps: ["Navigate to signup page", "Fill all required fields", "Submit registration form", "Verify success message or redirect"] },
|
|
22265
|
+
{ name: "Logout flow", description: "While authenticated, find and click the logout button/link, verify redirect to public page.", tags: ["auth"], priority: "medium", requiresAuth: true, steps: ["Click user menu or profile", "Click logout", "Verify redirect to login or home page"] }
|
|
22266
|
+
],
|
|
22267
|
+
crud: [
|
|
22268
|
+
{ name: "Create new item", description: "Navigate to the create form, fill all fields, submit, and verify the new item appears in the list.", tags: ["crud"], priority: "high", steps: ["Navigate to the list/index page", "Click create/add button", "Fill all required fields", "Submit the form", "Verify new item appears in list"] },
|
|
22269
|
+
{ name: "Read/view item details", description: "Click on an existing item to view its details page. Verify all fields are displayed correctly.", tags: ["crud"], priority: "medium", steps: ["Navigate to list page", "Click on an item", "Verify detail page shows all fields"] },
|
|
22270
|
+
{ name: "Update existing item", description: "Edit an existing item, change some fields, save, and verify changes persisted.", tags: ["crud"], priority: "high", steps: ["Navigate to item detail", "Click edit button", "Modify fields", "Save changes", "Verify updated values"] },
|
|
22271
|
+
{ name: "Delete item", description: "Delete an existing item and verify it's removed from the list.", tags: ["crud"], priority: "medium", steps: ["Navigate to list page", "Click delete on an item", "Confirm deletion", "Verify item removed from list"] }
|
|
22272
|
+
],
|
|
22273
|
+
forms: [
|
|
22274
|
+
{ name: "Form validation - empty submission", description: "Submit a form with all fields empty and verify validation errors appear for required fields.", tags: ["forms", "validation"], priority: "high", steps: ["Navigate to form page", "Click submit without filling fields", "Verify validation errors appear for each required field"] },
|
|
22275
|
+
{ name: "Form validation - invalid data", description: "Submit a form with invalid data (bad email, short password, etc) and verify appropriate error messages.", tags: ["forms", "validation"], priority: "high", steps: ["Navigate to form page", "Enter invalid email format", "Enter too-short password", "Submit form", "Verify specific validation error messages"] },
|
|
22276
|
+
{ name: "Form successful submission", description: "Fill form with valid data, submit, and verify success state (redirect, success message, or data saved).", tags: ["forms"], priority: "high", steps: ["Navigate to form page", "Fill all fields with valid data", "Submit form", "Verify success state"] }
|
|
22277
|
+
],
|
|
22278
|
+
nav: [
|
|
22279
|
+
{ name: "Main navigation links work", description: "Click through each main navigation link and verify each page loads correctly without errors.", tags: ["navigation", "smoke"], priority: "high", steps: ["Click each nav link", "Verify page loads", "Verify no error states", "Verify breadcrumbs if present"] },
|
|
22280
|
+
{ name: "Mobile navigation", description: "At mobile viewport, verify hamburger menu opens, navigation links are accessible, and pages load correctly.", tags: ["navigation", "responsive"], priority: "medium", steps: ["Resize to mobile viewport", "Click hamburger/menu icon", "Verify nav links appear", "Click a nav link", "Verify page loads"] }
|
|
22281
|
+
],
|
|
22282
|
+
a11y: [
|
|
22283
|
+
{ 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"] },
|
|
22284
|
+
{ 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"] }
|
|
22285
|
+
],
|
|
22286
|
+
checkout: [
|
|
22287
|
+
{ 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"] },
|
|
22288
|
+
{ 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"] },
|
|
22289
|
+
{ 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"] },
|
|
22290
|
+
{ 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"] }
|
|
22291
|
+
],
|
|
22292
|
+
search: [
|
|
22293
|
+
{ 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"] },
|
|
22294
|
+
{ 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)"] },
|
|
22295
|
+
{ 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"] },
|
|
22296
|
+
{ 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"] }
|
|
22297
|
+
]
|
|
22298
|
+
};
|
|
22299
|
+
function getTemplate(name) {
|
|
22300
|
+
return SCENARIO_TEMPLATES[name] ?? null;
|
|
22301
|
+
}
|
|
22302
|
+
function listTemplateNames() {
|
|
22303
|
+
return Object.keys(SCENARIO_TEMPLATES);
|
|
22304
|
+
}
|
|
22305
|
+
|
|
22306
|
+
// src/mcp/index.ts
|
|
22005
22307
|
init_runs();
|
|
22006
22308
|
init_results();
|
|
22007
22309
|
init_screenshots();
|
|
@@ -23078,15 +23380,81 @@ server.tool("create_scenario", "Create a new test scenario", {
|
|
|
23078
23380
|
priority: exports_external.enum(["low", "medium", "high", "critical"]).optional().describe("Scenario priority"),
|
|
23079
23381
|
model: exports_external.string().optional().describe(MODEL_DESC),
|
|
23080
23382
|
targetPath: exports_external.string().optional().describe("URL path to navigate to"),
|
|
23081
|
-
requiresAuth: exports_external.boolean().optional().describe("Whether scenario requires authentication")
|
|
23082
|
-
|
|
23383
|
+
requiresAuth: exports_external.boolean().optional().describe("Whether scenario requires authentication"),
|
|
23384
|
+
projectId: exports_external.string().optional().describe("Project ID to scope this scenario to")
|
|
23385
|
+
}, async ({ name, description, steps, tags, priority, model, targetPath, requiresAuth, projectId }) => {
|
|
23083
23386
|
try {
|
|
23084
|
-
const scenario = createScenario({ name, description, steps, tags, priority, model, targetPath, requiresAuth });
|
|
23387
|
+
const scenario = createScenario({ name, description, steps, tags, priority, model, targetPath, requiresAuth, projectId });
|
|
23085
23388
|
return json(scenario);
|
|
23086
23389
|
} catch (error) {
|
|
23087
23390
|
return errorResponse(error);
|
|
23088
23391
|
}
|
|
23089
23392
|
});
|
|
23393
|
+
server.tool("batch_create_scenarios", "Create multiple test scenarios in a single call. Each item requires name and description.", {
|
|
23394
|
+
scenarios: exports_external.array(exports_external.object({
|
|
23395
|
+
name: exports_external.string().describe("Scenario name"),
|
|
23396
|
+
description: exports_external.string().describe("What this scenario tests"),
|
|
23397
|
+
steps: exports_external.array(exports_external.string()).optional().describe("Ordered test steps"),
|
|
23398
|
+
tags: exports_external.array(exports_external.string()).optional().describe("Tags for filtering"),
|
|
23399
|
+
priority: exports_external.enum(["low", "medium", "high", "critical"]).optional().describe("Scenario priority"),
|
|
23400
|
+
model: exports_external.string().optional().describe(MODEL_DESC),
|
|
23401
|
+
targetPath: exports_external.string().optional().describe("URL path to navigate to"),
|
|
23402
|
+
requiresAuth: exports_external.boolean().optional().describe("Whether scenario requires authentication")
|
|
23403
|
+
})).min(1).max(100).describe("Array of scenarios to create"),
|
|
23404
|
+
projectId: exports_external.string().optional().describe("Project ID to scope all scenarios to")
|
|
23405
|
+
}, async ({ scenarios, projectId }) => {
|
|
23406
|
+
try {
|
|
23407
|
+
const results = [];
|
|
23408
|
+
for (const s of scenarios) {
|
|
23409
|
+
try {
|
|
23410
|
+
const scenario = createScenario({ ...s, projectId });
|
|
23411
|
+
results.push({ id: scenario.id, name: scenario.name, shortId: scenario.shortId });
|
|
23412
|
+
} catch (e) {
|
|
23413
|
+
results.push({ id: "", name: s.name, shortId: "", error: e instanceof Error ? e.message : String(e) });
|
|
23414
|
+
}
|
|
23415
|
+
}
|
|
23416
|
+
const created = results.filter((r) => !r.error).length;
|
|
23417
|
+
const failed = results.filter((r) => r.error).length;
|
|
23418
|
+
return json({ created, failed, total: scenarios.length, results });
|
|
23419
|
+
} catch (error) {
|
|
23420
|
+
return errorResponse(error);
|
|
23421
|
+
}
|
|
23422
|
+
});
|
|
23423
|
+
server.tool("list_templates", "List built-in scenario templates available for quick setup. Use apply_template to create scenarios from a template.", {}, async () => {
|
|
23424
|
+
try {
|
|
23425
|
+
const templates = listTemplateNames().map((name) => {
|
|
23426
|
+
const scenarios = getTemplate(name);
|
|
23427
|
+
return { name, scenarioCount: scenarios.length, scenarios: scenarios.map((s) => ({ name: s.name, description: s.description, priority: s.priority, tags: s.tags })) };
|
|
23428
|
+
});
|
|
23429
|
+
return json({ templates });
|
|
23430
|
+
} catch (error) {
|
|
23431
|
+
return errorResponse(error);
|
|
23432
|
+
}
|
|
23433
|
+
});
|
|
23434
|
+
server.tool("apply_template", "Create scenarios from a built-in template. Returns the created scenario IDs and any errors.", {
|
|
23435
|
+
template: exports_external.string().describe("Template name to apply (auth, crud, forms, nav, a11y, checkout, search)"),
|
|
23436
|
+
projectId: exports_external.string().optional().describe("Project ID to scope scenarios to")
|
|
23437
|
+
}, async ({ template, projectId }) => {
|
|
23438
|
+
try {
|
|
23439
|
+
const scenarios = getTemplate(template);
|
|
23440
|
+
if (!scenarios)
|
|
23441
|
+
return errorResponse(new Error(`Template not found: ${template}. Available: ${listTemplateNames().join(", ")}`));
|
|
23442
|
+
const results = [];
|
|
23443
|
+
for (const s of scenarios) {
|
|
23444
|
+
try {
|
|
23445
|
+
const scenario = createScenario({ ...s, projectId });
|
|
23446
|
+
results.push({ id: scenario.id, name: scenario.name, shortId: scenario.shortId });
|
|
23447
|
+
} catch (e) {
|
|
23448
|
+
results.push({ id: "", name: s.name, shortId: "", error: e instanceof Error ? e.message : String(e) });
|
|
23449
|
+
}
|
|
23450
|
+
}
|
|
23451
|
+
const created = results.filter((r) => !r.error).length;
|
|
23452
|
+
const failed = results.filter((r) => r.error).length;
|
|
23453
|
+
return json({ template, created, failed, total: scenarios.length, results });
|
|
23454
|
+
} catch (error) {
|
|
23455
|
+
return errorResponse(error);
|
|
23456
|
+
}
|
|
23457
|
+
});
|
|
23090
23458
|
server.tool("get_scenario", `Get a scenario by ID or short ID. ${ID_DESC}`, {
|
|
23091
23459
|
id: exports_external.string().describe(`Scenario ID or short ID. ${ID_DESC}`)
|
|
23092
23460
|
}, async ({ id }) => {
|
|
@@ -23157,12 +23525,15 @@ server.tool("run_scenarios", "Run test scenarios against a URL. Provide url dire
|
|
|
23157
23525
|
headed: exports_external.boolean().optional().describe("Run browser in headed mode"),
|
|
23158
23526
|
parallel: exports_external.number().optional().describe("Number of parallel workers"),
|
|
23159
23527
|
personaId: exports_external.string().optional().describe("Override persona ID for this run"),
|
|
23528
|
+
personaIds: exports_external.array(exports_external.string()).optional().describe("Run with multiple personas for divergence testing (each persona runs all scenarios)"),
|
|
23160
23529
|
samples: exports_external.number().int().min(1).max(20).optional().describe("Run each scenario N times for flakiness detection (default 1)"),
|
|
23161
23530
|
flakinessThreshold: exports_external.number().min(0).max(1).optional().describe("Pass rate below which scenario is marked flaky (default 0.95)"),
|
|
23162
23531
|
maxCostCents: exports_external.number().optional().describe("Hard budget cap in cents \u2014 run is rejected before starting if estimated cost exceeds this"),
|
|
23163
23532
|
cacheMaxAgeMs: exports_external.number().optional().describe("Skip scenarios that passed at the same URL within this many ms (0 = disabled)"),
|
|
23164
|
-
minimal: exports_external.boolean().optional().describe("Fastest mode: cheapest model, max parallelism, min turns \u2014 ideal for CI smoke checks")
|
|
23165
|
-
|
|
23533
|
+
minimal: exports_external.boolean().optional().describe("Fastest mode: cheapest model, max parallelism, min turns \u2014 ideal for CI smoke checks"),
|
|
23534
|
+
timeoutMs: exports_external.number().optional().describe("Per-scenario timeout in ms (default 120000)"),
|
|
23535
|
+
recordVideo: exports_external.boolean().optional().describe("Record video of each scenario run (Playwright only)")
|
|
23536
|
+
}, async ({ url, env: env2, tags, scenarioIds, priority, model, headed, parallel, personaId, personaIds, samples, flakinessThreshold, maxCostCents, cacheMaxAgeMs, minimal, timeoutMs, recordVideo }) => {
|
|
23166
23537
|
try {
|
|
23167
23538
|
let resolvedUrl = url;
|
|
23168
23539
|
if (!resolvedUrl && env2) {
|
|
@@ -23180,12 +23551,54 @@ server.tool("run_scenarios", "Run test scenarios against a URL. Provide url dire
|
|
|
23180
23551
|
}
|
|
23181
23552
|
if (!resolvedUrl)
|
|
23182
23553
|
return errorResponse(new Error("No URL provided and no default environment set. Pass url or env."));
|
|
23183
|
-
const { runId, scenarioCount } = startRunAsync({ url: resolvedUrl, tags, scenarioIds, priority, model, headed, parallel, personaId, samples, flakinessThreshold, maxCostCents, cacheMaxAgeMs, minimal });
|
|
23554
|
+
const { runId, scenarioCount } = startRunAsync({ url: resolvedUrl, tags, scenarioIds, priority, model, headed, parallel, personaId, personaIds, samples, flakinessThreshold, maxCostCents, cacheMaxAgeMs, minimal, timeout: timeoutMs, recordVideo });
|
|
23184
23555
|
return json({ runId, scenarioCount, url: resolvedUrl, status: "running", message: "Poll with get_run to check progress." });
|
|
23185
23556
|
} catch (error) {
|
|
23186
23557
|
return errorResponse(error);
|
|
23187
23558
|
}
|
|
23188
23559
|
});
|
|
23560
|
+
server.tool("retry_failed", "Re-run only failed/errored scenarios from a previous run. Creates a new run with only the failing scenarios.", {
|
|
23561
|
+
runId: exports_external.string().describe("Previous run ID to retry failures from"),
|
|
23562
|
+
url: exports_external.string().optional().describe("Target URL (overrides original run URL)"),
|
|
23563
|
+
model: exports_external.string().optional().describe(MODEL_DESC),
|
|
23564
|
+
headed: exports_external.boolean().optional().describe("Run browser in headed mode"),
|
|
23565
|
+
parallel: exports_external.number().optional().describe("Number of parallel workers"),
|
|
23566
|
+
maxRetries: exports_external.number().int().min(0).max(3).optional().describe("Max retries per failed scenario"),
|
|
23567
|
+
maxCostCents: exports_external.number().optional().describe("Hard budget cap in cents")
|
|
23568
|
+
}, async ({ runId, url, model, headed, parallel, maxRetries, maxCostCents }) => {
|
|
23569
|
+
try {
|
|
23570
|
+
const run = getRun(runId);
|
|
23571
|
+
if (!run)
|
|
23572
|
+
return errorResponse(notFoundErr(runId, "Run"));
|
|
23573
|
+
if (run.status !== "failed")
|
|
23574
|
+
return errorResponse(new Error("Run is not in failed state. Can only retry failures from a failed run."));
|
|
23575
|
+
const results = getResultsByRun(runId);
|
|
23576
|
+
const failedResultIds = results.filter((r) => r.status === "failed" || r.status === "error").map((r) => r.scenarioId);
|
|
23577
|
+
if (failedResultIds.length === 0)
|
|
23578
|
+
return errorResponse(new Error("No failed results found in this run."));
|
|
23579
|
+
const resolvedUrl = url ?? run.url;
|
|
23580
|
+
const { runId: newRunId, scenarioCount } = startRunAsync({
|
|
23581
|
+
url: resolvedUrl,
|
|
23582
|
+
scenarioIds: failedResultIds,
|
|
23583
|
+
model,
|
|
23584
|
+
headed,
|
|
23585
|
+
parallel,
|
|
23586
|
+
retry: maxRetries ?? 0,
|
|
23587
|
+
maxCostCents
|
|
23588
|
+
});
|
|
23589
|
+
return json({
|
|
23590
|
+
runId: newRunId,
|
|
23591
|
+
originalRunId: runId,
|
|
23592
|
+
scenarioCount,
|
|
23593
|
+
retriedScenarioIds: failedResultIds,
|
|
23594
|
+
url: resolvedUrl,
|
|
23595
|
+
status: "running",
|
|
23596
|
+
message: "Poll with get_run to check progress."
|
|
23597
|
+
});
|
|
23598
|
+
} catch (error) {
|
|
23599
|
+
return errorResponse(error);
|
|
23600
|
+
}
|
|
23601
|
+
});
|
|
23189
23602
|
server.tool("get_run", `Get details of a test run. ${ID_DESC}`, {
|
|
23190
23603
|
id: exports_external.string().describe(`Run ID. ${ID_DESC}`)
|
|
23191
23604
|
}, async ({ id }) => {
|
|
@@ -23199,11 +23612,17 @@ server.tool("get_run", `Get details of a test run. ${ID_DESC}`, {
|
|
|
23199
23612
|
}
|
|
23200
23613
|
});
|
|
23201
23614
|
server.tool("list_runs", "List test runs with optional filters", {
|
|
23615
|
+
projectId: exports_external.string().optional().describe("Filter by project ID"),
|
|
23202
23616
|
status: exports_external.enum(["pending", "running", "passed", "failed", "cancelled"]).optional().describe("Filter by status"),
|
|
23203
|
-
|
|
23204
|
-
|
|
23617
|
+
since: exports_external.string().optional().describe("Filter runs started at or after this ISO date"),
|
|
23618
|
+
until: exports_external.string().optional().describe("Filter runs started at or before this ISO date"),
|
|
23619
|
+
sort: exports_external.enum(["date", "duration", "cost"]).optional().describe("Sort field"),
|
|
23620
|
+
desc: exports_external.boolean().optional().describe("Sort descending (default true)"),
|
|
23621
|
+
limit: exports_external.number().optional().describe("Max results to return"),
|
|
23622
|
+
offset: exports_external.number().optional().describe("Number of results to skip")
|
|
23623
|
+
}, async ({ projectId, status, since, until, sort, desc, limit, offset }) => {
|
|
23205
23624
|
try {
|
|
23206
|
-
const runs = listRuns({ status, limit });
|
|
23625
|
+
const runs = listRuns({ projectId, status, since, until, sort, desc, limit, offset });
|
|
23207
23626
|
return json({ items: runs, total: runs.length });
|
|
23208
23627
|
} catch (error) {
|
|
23209
23628
|
return errorResponse(error);
|
|
@@ -23597,14 +24016,15 @@ server.tool("run_health_scan", "Run all scanners (console, network, links, perfo
|
|
|
23597
24016
|
url: exports_external.string().describe("URL to scan"),
|
|
23598
24017
|
pages: exports_external.array(exports_external.string()).optional().describe("Specific paths to include"),
|
|
23599
24018
|
projectId: exports_external.string().optional().describe("Project ID"),
|
|
23600
|
-
scanners: exports_external.array(exports_external.enum(["console", "network", "links", "performance"])).optional().describe("Which scanners to run (default: console, network, links)"),
|
|
24019
|
+
scanners: exports_external.array(exports_external.enum(["console", "network", "links", "performance", "a11y"])).optional().describe("Which scanners to run (default: console, network, links)"),
|
|
23601
24020
|
maxPages: exports_external.number().optional().describe("Max pages for link crawl (default 20)"),
|
|
24021
|
+
wcagLevel: exports_external.enum(["A", "AA", "AAA"]).optional().describe("WCAG compliance level for a11y scanner (default: AA)"),
|
|
23602
24022
|
headed: exports_external.boolean().optional(),
|
|
23603
24023
|
timeoutMs: exports_external.number().optional()
|
|
23604
|
-
}, async ({ url, pages, projectId, scanners, maxPages, headed, timeoutMs }) => {
|
|
24024
|
+
}, async ({ url, pages, projectId, scanners, maxPages, headed, timeoutMs, wcagLevel }) => {
|
|
23605
24025
|
try {
|
|
23606
24026
|
const { runHealthScan: runHealthScan2 } = await Promise.resolve().then(() => (init_health_scan(), exports_health_scan));
|
|
23607
|
-
const summary = await runHealthScan2({ url, pages, projectId, scanners, maxPages, headed, timeoutMs });
|
|
24027
|
+
const summary = await runHealthScan2({ url, pages, projectId, scanners, maxPages, headed, timeoutMs, wcagLevel });
|
|
23608
24028
|
return json(summary);
|
|
23609
24029
|
} catch (e) {
|
|
23610
24030
|
return errorResponse(e);
|
|
@@ -24362,6 +24782,32 @@ Context: ${context}` : ""}`,
|
|
|
24362
24782
|
return errorResponse(e);
|
|
24363
24783
|
}
|
|
24364
24784
|
});
|
|
24785
|
+
server.tool("get_har", `Get HAR (HTTP Archive) file for a test result. Returns metadata by default, or full HAR content when includeContent is true. Useful for debugging network requests, API calls, and CORS issues. ${ID_DESC}`, {
|
|
24786
|
+
resultId: exports_external.string().describe(`Result ID. ${ID_DESC}`),
|
|
24787
|
+
includeContent: exports_external.boolean().optional().describe("Return full HAR JSON content (default: false)")
|
|
24788
|
+
}, async ({ resultId, includeContent }) => {
|
|
24789
|
+
try {
|
|
24790
|
+
const { getResult: getResult2 } = await Promise.resolve().then(() => (init_results(), exports_results));
|
|
24791
|
+
const result = getResult2(resultId);
|
|
24792
|
+
if (!result)
|
|
24793
|
+
return errorResponse(notFoundErr(resultId, "Result"));
|
|
24794
|
+
const harPath = result.harPath ?? result.metadata?.harPath;
|
|
24795
|
+
if (!harPath)
|
|
24796
|
+
return json({ resultId, harAvailable: false, message: "No HAR file recorded for this result." });
|
|
24797
|
+
const harFile = Bun.file(harPath);
|
|
24798
|
+
const exists = await harFile.exists();
|
|
24799
|
+
if (!exists)
|
|
24800
|
+
return json({ resultId, harPath, harAvailable: false, message: "HAR file was recorded but has been cleaned up." });
|
|
24801
|
+
if (!includeContent) {
|
|
24802
|
+
const size = await harFile.size();
|
|
24803
|
+
return json({ resultId, harPath, harAvailable: true, sizeBytes: size, message: "HAR file available. Use includeContent: true to retrieve." });
|
|
24804
|
+
}
|
|
24805
|
+
const harContent = await harFile.text();
|
|
24806
|
+
return json({ resultId, harPath, harAvailable: true, har: JSON.parse(harContent) });
|
|
24807
|
+
} catch (e) {
|
|
24808
|
+
return errorResponse(e);
|
|
24809
|
+
}
|
|
24810
|
+
});
|
|
24365
24811
|
server.tool("wait_for_run", "Poll a run until it reaches a terminal state (passed/failed/cancelled) and return the final results summary. Synchronous \u2014 blocks until complete or timeout.", {
|
|
24366
24812
|
runId: exports_external.string().describe("Run ID to wait for"),
|
|
24367
24813
|
timeoutMs: exports_external.number().optional().default(300000).describe("Max wait time in ms (default 5 minutes)"),
|
|
@@ -24644,6 +25090,13 @@ server.tool("list_scenarios_by_page", "Group scenarios by page (targetPath). Sho
|
|
|
24644
25090
|
}
|
|
24645
25091
|
});
|
|
24646
25092
|
registerCloudTools(server, "testers");
|
|
25093
|
+
process.on("unhandledRejection", (reason) => {
|
|
25094
|
+
const msg = reason instanceof Error ? reason.stack ?? reason.message : String(reason);
|
|
25095
|
+
console.error(`[testers-mcp] Unhandled promise rejection: ${msg}`);
|
|
25096
|
+
});
|
|
25097
|
+
process.on("uncaughtException", (err) => {
|
|
25098
|
+
console.error(`[testers-mcp] Uncaught exception: ${err.stack ?? err.message}`);
|
|
25099
|
+
});
|
|
24647
25100
|
async function main() {
|
|
24648
25101
|
const transport = new StdioServerTransport;
|
|
24649
25102
|
await server.connect(transport);
|