@hasna/testers 0.0.28 → 0.0.30
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 +92 -0
- package/dist/cli/index.js +1354 -79
- 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 +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +818 -25
- 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 +6 -7
- 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/config.d.ts.map +1 -1
- package/dist/lib/discovery.d.ts +33 -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/hybrid-runner.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/prod-debug.d.ts +77 -0
- package/dist/lib/prod-debug.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 +852 -74
- package/dist/sdk/index.d.ts +48 -0
- package/dist/sdk/index.d.ts.map +1 -0
- package/dist/server/index.js +276 -29
- package/dist/types/index.d.ts +66 -2
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +2 -2
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"),
|
|
@@ -12167,7 +12253,8 @@ function loadConfig() {
|
|
|
12167
12253
|
judgeModel: fileConfig.judgeModel,
|
|
12168
12254
|
judgeProvider: fileConfig.judgeProvider,
|
|
12169
12255
|
selfHeal: fileConfig.selfHeal ?? false,
|
|
12170
|
-
conversationsSpace: fileConfig.conversationsSpace
|
|
12256
|
+
conversationsSpace: fileConfig.conversationsSpace,
|
|
12257
|
+
prodDebug: fileConfig.prodDebug
|
|
12171
12258
|
};
|
|
12172
12259
|
const envModel = process.env["TESTERS_MODEL"];
|
|
12173
12260
|
if (envModel) {
|
|
@@ -14551,6 +14638,76 @@ var init_costs = __esm(() => {
|
|
|
14551
14638
|
};
|
|
14552
14639
|
});
|
|
14553
14640
|
|
|
14641
|
+
// src/db/step-results.ts
|
|
14642
|
+
function createStepResult(input) {
|
|
14643
|
+
const db2 = getDatabase();
|
|
14644
|
+
const id = uuid();
|
|
14645
|
+
const timestamp = now();
|
|
14646
|
+
db2.query(`
|
|
14647
|
+
INSERT INTO step_results (id, result_id, step_number, action, status, tool_name, tool_input, thinking, created_at)
|
|
14648
|
+
VALUES (?, ?, ?, ?, 'running', ?, ?, ?, ?)
|
|
14649
|
+
`).run(id, input.resultId, input.stepNumber, input.action, input.toolName ?? null, input.toolInput ? JSON.stringify(input.toolInput) : null, input.thinking ?? null, timestamp);
|
|
14650
|
+
return getStepResult(id);
|
|
14651
|
+
}
|
|
14652
|
+
function getStepResult(id) {
|
|
14653
|
+
const db2 = getDatabase();
|
|
14654
|
+
const row = db2.query("SELECT * FROM step_results WHERE id = ?").get(id);
|
|
14655
|
+
return row ? stepResultFromRow(row) : null;
|
|
14656
|
+
}
|
|
14657
|
+
function updateStepResult(id, updates) {
|
|
14658
|
+
const db2 = getDatabase();
|
|
14659
|
+
const existing = getStepResult(id);
|
|
14660
|
+
if (!existing)
|
|
14661
|
+
return null;
|
|
14662
|
+
const sets = [];
|
|
14663
|
+
const params = [];
|
|
14664
|
+
if (updates.status !== undefined) {
|
|
14665
|
+
sets.push("status = ?");
|
|
14666
|
+
params.push(updates.status);
|
|
14667
|
+
}
|
|
14668
|
+
if (updates.toolResult !== undefined) {
|
|
14669
|
+
sets.push("tool_result = ?");
|
|
14670
|
+
params.push(updates.toolResult);
|
|
14671
|
+
}
|
|
14672
|
+
if (updates.error !== undefined) {
|
|
14673
|
+
sets.push("error = ?");
|
|
14674
|
+
params.push(updates.error);
|
|
14675
|
+
}
|
|
14676
|
+
if (updates.durationMs !== undefined) {
|
|
14677
|
+
sets.push("duration_ms = ?");
|
|
14678
|
+
params.push(updates.durationMs);
|
|
14679
|
+
}
|
|
14680
|
+
if (updates.screenshotId !== undefined) {
|
|
14681
|
+
sets.push("screenshot_id = ?");
|
|
14682
|
+
params.push(updates.screenshotId);
|
|
14683
|
+
}
|
|
14684
|
+
if (sets.length === 0)
|
|
14685
|
+
return existing;
|
|
14686
|
+
params.push(id);
|
|
14687
|
+
db2.query(`UPDATE step_results SET ${sets.join(", ")} WHERE id = ?`).run(...params);
|
|
14688
|
+
return getStepResult(id);
|
|
14689
|
+
}
|
|
14690
|
+
function stepResultFromRow(row) {
|
|
14691
|
+
return {
|
|
14692
|
+
id: row.id,
|
|
14693
|
+
resultId: row.result_id,
|
|
14694
|
+
stepNumber: row.step_number,
|
|
14695
|
+
action: row.action,
|
|
14696
|
+
status: row.status,
|
|
14697
|
+
toolName: row.tool_name,
|
|
14698
|
+
toolInput: row.tool_input ? JSON.parse(row.tool_input) : null,
|
|
14699
|
+
toolResult: row.tool_result,
|
|
14700
|
+
thinking: row.thinking,
|
|
14701
|
+
error: row.error,
|
|
14702
|
+
durationMs: row.duration_ms,
|
|
14703
|
+
screenshotId: row.screenshot_id,
|
|
14704
|
+
createdAt: row.created_at
|
|
14705
|
+
};
|
|
14706
|
+
}
|
|
14707
|
+
var init_step_results = __esm(() => {
|
|
14708
|
+
init_database();
|
|
14709
|
+
});
|
|
14710
|
+
|
|
14554
14711
|
// src/db/personas.ts
|
|
14555
14712
|
function createPersona(input) {
|
|
14556
14713
|
const db2 = getDatabase();
|
|
@@ -14558,9 +14715,9 @@ function createPersona(input) {
|
|
|
14558
14715
|
const short_id = shortUuid();
|
|
14559
14716
|
const timestamp = now();
|
|
14560
14717
|
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);
|
|
14718
|
+
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)
|
|
14719
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?, ?)
|
|
14720
|
+
`).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
14721
|
return getPersona(id);
|
|
14565
14722
|
}
|
|
14566
14723
|
function getPersona(id) {
|
|
@@ -14678,6 +14835,18 @@ function updatePersona(id, updates, version) {
|
|
|
14678
14835
|
sets.push("auth_cookies = ?");
|
|
14679
14836
|
params.push(updates.authCookies ? JSON.stringify(updates.authCookies) : null);
|
|
14680
14837
|
}
|
|
14838
|
+
if (updates.authStrategy !== undefined) {
|
|
14839
|
+
sets.push("auth_strategy = ?");
|
|
14840
|
+
params.push(updates.authStrategy);
|
|
14841
|
+
}
|
|
14842
|
+
if (updates.authHeaders !== undefined) {
|
|
14843
|
+
sets.push("auth_headers = ?");
|
|
14844
|
+
params.push(JSON.stringify(updates.authHeaders));
|
|
14845
|
+
}
|
|
14846
|
+
if (updates.authCustomScript !== undefined) {
|
|
14847
|
+
sets.push("auth_script = ?");
|
|
14848
|
+
params.push(updates.authCustomScript);
|
|
14849
|
+
}
|
|
14681
14850
|
if (sets.length === 0) {
|
|
14682
14851
|
return existing;
|
|
14683
14852
|
}
|
|
@@ -14937,6 +15106,11 @@ function resolveCredential(value) {
|
|
|
14937
15106
|
}
|
|
14938
15107
|
return value;
|
|
14939
15108
|
}
|
|
15109
|
+
function isCredentialReference(value) {
|
|
15110
|
+
if (!value)
|
|
15111
|
+
return false;
|
|
15112
|
+
return value.startsWith("@secrets:") || value.startsWith("$");
|
|
15113
|
+
}
|
|
14940
15114
|
var init_secrets_resolver = () => {};
|
|
14941
15115
|
|
|
14942
15116
|
// src/lib/persona-auth.ts
|
|
@@ -15199,6 +15373,24 @@ function signPayload(body, secret) {
|
|
|
15199
15373
|
}
|
|
15200
15374
|
return `sha256=${Math.abs(hash).toString(16).padStart(16, "0")}`;
|
|
15201
15375
|
}
|
|
15376
|
+
function formatDiscordPayload(payload) {
|
|
15377
|
+
const isPassed = payload.run.status === "passed";
|
|
15378
|
+
const color = isPassed ? 2278750 : 15680580;
|
|
15379
|
+
return {
|
|
15380
|
+
username: "open-testers",
|
|
15381
|
+
embeds: [
|
|
15382
|
+
{
|
|
15383
|
+
title: `Test Run ${payload.run.status.toUpperCase()}`,
|
|
15384
|
+
color,
|
|
15385
|
+
description: `URL: ${payload.run.url}
|
|
15386
|
+
` + `Results: ${payload.run.passed}/${payload.run.total} passed` + (payload.run.failed > 0 ? ` (${payload.run.failed} failed)` : "") + (payload.schedule ? `
|
|
15387
|
+
Schedule: ${payload.schedule.name}` : ""),
|
|
15388
|
+
timestamp: payload.timestamp,
|
|
15389
|
+
footer: { text: "open-testers" }
|
|
15390
|
+
}
|
|
15391
|
+
]
|
|
15392
|
+
};
|
|
15393
|
+
}
|
|
15202
15394
|
function formatSlackPayload(payload) {
|
|
15203
15395
|
const status = payload.run.status === "passed" ? ":white_check_mark:" : ":x:";
|
|
15204
15396
|
const color = payload.run.status === "passed" ? "#22c55e" : "#ef4444";
|
|
@@ -15241,7 +15433,8 @@ async function dispatchWebhooks(event, run, schedule) {
|
|
|
15241
15433
|
if (!webhook.events.includes(event) && !webhook.events.includes("*"))
|
|
15242
15434
|
continue;
|
|
15243
15435
|
const isSlack = webhook.url.includes("hooks.slack.com");
|
|
15244
|
-
const
|
|
15436
|
+
const isDiscord = webhook.url.includes("discord.com/api/webhooks") || webhook.url.includes("discordapp.com/api/webhooks");
|
|
15437
|
+
const body = isSlack ? JSON.stringify(formatSlackPayload(payload)) : isDiscord ? JSON.stringify(formatDiscordPayload(payload)) : JSON.stringify(payload);
|
|
15245
15438
|
const headers = {
|
|
15246
15439
|
"Content-Type": "application/json"
|
|
15247
15440
|
};
|
|
@@ -15751,6 +15944,8 @@ __export(exports_runner, {
|
|
|
15751
15944
|
runBatch: () => runBatch,
|
|
15752
15945
|
onRunEvent: () => onRunEvent
|
|
15753
15946
|
});
|
|
15947
|
+
import { mkdirSync as mkdirSync8 } from "fs";
|
|
15948
|
+
import { join as join13 } from "path";
|
|
15754
15949
|
import { enableNetworkLogging } from "@hasna/browser";
|
|
15755
15950
|
function onRunEvent(handler) {
|
|
15756
15951
|
eventHandler = handler;
|
|
@@ -15836,13 +16031,35 @@ async function runSingleScenario(scenario, runId, options) {
|
|
|
15836
16031
|
emit({ type: "scenario:start", scenarioId: scenario.id, scenarioName: scenario.name, resultId: result.id, runId });
|
|
15837
16032
|
let browser = null;
|
|
15838
16033
|
let page = null;
|
|
16034
|
+
let context = null;
|
|
16035
|
+
let harPath = null;
|
|
15839
16036
|
let stopNetworkLogging = null;
|
|
15840
16037
|
const networkErrors = [];
|
|
15841
16038
|
try {
|
|
15842
16039
|
browser = await launchBrowser({ headless: !(effectiveOptions.headed ?? false), engine: effectiveOptions.engine });
|
|
15843
|
-
|
|
15844
|
-
|
|
15845
|
-
|
|
16040
|
+
const useHar = effectiveOptions.engine !== "lightpanda" && effectiveOptions.engine !== "bun";
|
|
16041
|
+
if (useHar) {
|
|
16042
|
+
const testersDir = process.env["HASNA_TESTERS_DIR"] || join13(process.env["HOME"] || "", ".hasna", "testers");
|
|
16043
|
+
const harDir = join13(testersDir, "hars");
|
|
16044
|
+
mkdirSync8(harDir, { recursive: true });
|
|
16045
|
+
harPath = join13(harDir, `${result.id}.har`);
|
|
16046
|
+
const contextOptions = {
|
|
16047
|
+
viewport: config.browser.viewport,
|
|
16048
|
+
recordHar: { path: harPath, mode: "full" }
|
|
16049
|
+
};
|
|
16050
|
+
if (effectiveOptions.recordVideo) {
|
|
16051
|
+
const videoDir = join13(testersDir, "videos");
|
|
16052
|
+
mkdirSync8(videoDir, { recursive: true });
|
|
16053
|
+
contextOptions.recordVideo = { dir: videoDir, size: config.browser.viewport };
|
|
16054
|
+
}
|
|
16055
|
+
context = await browser.newContext(contextOptions);
|
|
16056
|
+
page = await context.newPage();
|
|
16057
|
+
} else {
|
|
16058
|
+
page = await getPage(browser, {
|
|
16059
|
+
viewport: config.browser.viewport,
|
|
16060
|
+
engine: effectiveOptions.engine
|
|
16061
|
+
});
|
|
16062
|
+
}
|
|
15846
16063
|
const targetUrl = scenario.targetPath ? `${options.url.replace(/\/$/, "")}${scenario.targetPath}` : options.url;
|
|
15847
16064
|
const scenarioTimeout = scenario.timeoutMs ?? options.timeout ?? config.browser.timeout ?? 60000;
|
|
15848
16065
|
registerSession({
|
|
@@ -15862,7 +16079,11 @@ async function runSingleScenario(scenario, runId, options) {
|
|
|
15862
16079
|
}
|
|
15863
16080
|
});
|
|
15864
16081
|
const consoleErrors = [];
|
|
16082
|
+
const consoleLogs = [];
|
|
16083
|
+
let currentStep = 0;
|
|
15865
16084
|
page.on("console", (msg) => {
|
|
16085
|
+
const logEntry = { step: currentStep > 0 ? currentStep : null, type: msg.type(), text: msg.text(), timestamp: Date.now() };
|
|
16086
|
+
consoleLogs.push(logEntry);
|
|
15866
16087
|
if (msg.type() === "error")
|
|
15867
16088
|
consoleErrors.push(msg.text());
|
|
15868
16089
|
});
|
|
@@ -15894,6 +16115,7 @@ async function runSingleScenario(scenario, runId, options) {
|
|
|
15894
16115
|
}
|
|
15895
16116
|
await page.goto(targetUrl, { timeout: Math.min(scenarioTimeout, 30000) });
|
|
15896
16117
|
const stepStartTimes = new Map;
|
|
16118
|
+
const stepResultIds = new Map;
|
|
15897
16119
|
const agentResult = await withTimeout(runAgentLoop({
|
|
15898
16120
|
client,
|
|
15899
16121
|
page,
|
|
@@ -15916,13 +16138,32 @@ async function runSingleScenario(scenario, runId, options) {
|
|
|
15916
16138
|
onStep: (stepEvent) => {
|
|
15917
16139
|
let stepDurationMs;
|
|
15918
16140
|
if (stepEvent.type === "tool_call") {
|
|
16141
|
+
currentStep = stepEvent.stepNumber;
|
|
15919
16142
|
stepStartTimes.set(stepEvent.stepNumber, Date.now());
|
|
16143
|
+
const stepResult = createStepResult({
|
|
16144
|
+
resultId: result.id,
|
|
16145
|
+
stepNumber: stepEvent.stepNumber,
|
|
16146
|
+
action: stepEvent.toolName ?? `step-${stepEvent.stepNumber}`,
|
|
16147
|
+
toolName: stepEvent.toolName,
|
|
16148
|
+
toolInput: stepEvent.toolInput,
|
|
16149
|
+
thinking: stepEvent.thinking
|
|
16150
|
+
});
|
|
16151
|
+
stepResultIds.set(stepEvent.stepNumber, stepResult.id);
|
|
15920
16152
|
} else if (stepEvent.type === "tool_result") {
|
|
15921
16153
|
const startTime = stepStartTimes.get(stepEvent.stepNumber);
|
|
15922
16154
|
if (startTime !== undefined) {
|
|
15923
16155
|
stepDurationMs = Date.now() - startTime;
|
|
15924
16156
|
stepStartTimes.delete(stepEvent.stepNumber);
|
|
15925
16157
|
}
|
|
16158
|
+
const stepResultId = stepResultIds.get(stepEvent.stepNumber);
|
|
16159
|
+
if (stepResultId) {
|
|
16160
|
+
const isSuccess = !stepEvent.toolResult?.toLowerCase().includes("error") && !stepEvent.toolResult?.toLowerCase().includes("failed");
|
|
16161
|
+
updateStepResult(stepResultId, {
|
|
16162
|
+
status: isSuccess ? "passed" : "failed",
|
|
16163
|
+
toolResult: stepEvent.toolResult,
|
|
16164
|
+
durationMs: stepDurationMs
|
|
16165
|
+
});
|
|
16166
|
+
}
|
|
15926
16167
|
}
|
|
15927
16168
|
emit({
|
|
15928
16169
|
type: `step:${stepEvent.type}`,
|
|
@@ -15971,7 +16212,7 @@ async function runSingleScenario(scenario, runId, options) {
|
|
|
15971
16212
|
durationMs: Date.now() - new Date(result.createdAt).getTime(),
|
|
15972
16213
|
tokensUsed: agentResult.tokensUsed,
|
|
15973
16214
|
costCents: estimateCost(model, agentResult.tokensUsed),
|
|
15974
|
-
metadata: networkErrors.length > 0 ? networkMeta :
|
|
16215
|
+
metadata: { consoleLogs, ...networkErrors.length > 0 ? networkMeta : {} }
|
|
15975
16216
|
});
|
|
15976
16217
|
if (agentResult.status === "failed" || agentResult.status === "error") {
|
|
15977
16218
|
const failureAnalysis = analyzeFailure(null, agentResult.reasoning ?? null);
|
|
@@ -16007,8 +16248,16 @@ async function runSingleScenario(scenario, runId, options) {
|
|
|
16007
16248
|
emit({ type: "scenario:error", scenarioId: scenario.id, scenarioName: scenario.name, error: errorMsg, runId });
|
|
16008
16249
|
return updatedResult;
|
|
16009
16250
|
} finally {
|
|
16010
|
-
if (
|
|
16011
|
-
|
|
16251
|
+
if (harPath) {
|
|
16252
|
+
try {
|
|
16253
|
+
updateResult(result.id, { metadata: { harPath } });
|
|
16254
|
+
} catch {}
|
|
16255
|
+
}
|
|
16256
|
+
if (browser) {
|
|
16257
|
+
try {
|
|
16258
|
+
await closeBrowser(browser, effectiveOptions.engine);
|
|
16259
|
+
} catch {}
|
|
16260
|
+
}
|
|
16012
16261
|
}
|
|
16013
16262
|
}
|
|
16014
16263
|
async function runBatch(scenarios, options) {
|
|
@@ -16304,6 +16553,7 @@ var init_runner = __esm(() => {
|
|
|
16304
16553
|
init_results();
|
|
16305
16554
|
init_costs();
|
|
16306
16555
|
init_screenshots();
|
|
16556
|
+
init_step_results();
|
|
16307
16557
|
init_scenarios();
|
|
16308
16558
|
init_personas();
|
|
16309
16559
|
init_browser();
|
|
@@ -17341,6 +17591,16 @@ async function runHealthScan(options) {
|
|
|
17341
17591
|
});
|
|
17342
17592
|
results.push(piiResult);
|
|
17343
17593
|
}
|
|
17594
|
+
if (scanners.includes("a11y")) {
|
|
17595
|
+
const a11yResult = await scanA11y({
|
|
17596
|
+
url,
|
|
17597
|
+
pages,
|
|
17598
|
+
wcagLevel: options.wcagLevel ?? "AA",
|
|
17599
|
+
headed,
|
|
17600
|
+
timeoutMs
|
|
17601
|
+
});
|
|
17602
|
+
results.push(a11yResult);
|
|
17603
|
+
}
|
|
17344
17604
|
const allIssues = results.flatMap((r) => r.issues);
|
|
17345
17605
|
let newCount = 0;
|
|
17346
17606
|
let regressedCount = 0;
|
|
@@ -17556,10 +17816,10 @@ __export(exports_contacts_connector, {
|
|
|
17556
17816
|
function getContactsDb() {
|
|
17557
17817
|
const { Database: Database4 } = __require("bun:sqlite");
|
|
17558
17818
|
const { existsSync: existsSync5 } = __require("fs");
|
|
17559
|
-
const { join:
|
|
17819
|
+
const { join: join14 } = __require("path");
|
|
17560
17820
|
const { homedir: homedir7 } = __require("os");
|
|
17561
17821
|
const envPath = process.env["HASNA_CONTACTS_DB_PATH"] ?? process.env["OPEN_CONTACTS_DB"];
|
|
17562
|
-
const dbPath = envPath ??
|
|
17822
|
+
const dbPath = envPath ?? join14(homedir7(), ".hasna", "contacts", "contacts.db");
|
|
17563
17823
|
if (!existsSync5(dbPath))
|
|
17564
17824
|
return null;
|
|
17565
17825
|
const db2 = new Database4(dbPath, { readonly: true });
|
|
@@ -17680,7 +17940,7 @@ __export(exports_army_runner, {
|
|
|
17680
17940
|
waitForArmyRun: () => waitForArmyRun,
|
|
17681
17941
|
runWithArmy: () => runWithArmy
|
|
17682
17942
|
});
|
|
17683
|
-
import { join as
|
|
17943
|
+
import { join as join14 } from "path";
|
|
17684
17944
|
function chunkArray(arr, n) {
|
|
17685
17945
|
const chunks = [];
|
|
17686
17946
|
const size = Math.ceil(arr.length / n);
|
|
@@ -17690,7 +17950,7 @@ function chunkArray(arr, n) {
|
|
|
17690
17950
|
return chunks;
|
|
17691
17951
|
}
|
|
17692
17952
|
function getCliPath() {
|
|
17693
|
-
const srcPath =
|
|
17953
|
+
const srcPath = join14(import.meta.dir, "../cli/index.tsx");
|
|
17694
17954
|
return srcPath;
|
|
17695
17955
|
}
|
|
17696
17956
|
async function runWithArmy(options) {
|
|
@@ -22002,6 +22262,54 @@ var NEVER = INVALID;
|
|
|
22002
22262
|
// src/mcp/index.ts
|
|
22003
22263
|
init_dist();
|
|
22004
22264
|
init_scenarios();
|
|
22265
|
+
|
|
22266
|
+
// src/lib/templates.ts
|
|
22267
|
+
var SCENARIO_TEMPLATES = {
|
|
22268
|
+
auth: [
|
|
22269
|
+
{ 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"] },
|
|
22270
|
+
{ 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"] },
|
|
22271
|
+
{ 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"] }
|
|
22272
|
+
],
|
|
22273
|
+
crud: [
|
|
22274
|
+
{ 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"] },
|
|
22275
|
+
{ 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"] },
|
|
22276
|
+
{ 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"] },
|
|
22277
|
+
{ 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"] }
|
|
22278
|
+
],
|
|
22279
|
+
forms: [
|
|
22280
|
+
{ 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"] },
|
|
22281
|
+
{ 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"] },
|
|
22282
|
+
{ 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"] }
|
|
22283
|
+
],
|
|
22284
|
+
nav: [
|
|
22285
|
+
{ 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"] },
|
|
22286
|
+
{ 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"] }
|
|
22287
|
+
],
|
|
22288
|
+
a11y: [
|
|
22289
|
+
{ 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"] },
|
|
22290
|
+
{ 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"] }
|
|
22291
|
+
],
|
|
22292
|
+
checkout: [
|
|
22293
|
+
{ 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"] },
|
|
22294
|
+
{ 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"] },
|
|
22295
|
+
{ 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"] },
|
|
22296
|
+
{ 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"] }
|
|
22297
|
+
],
|
|
22298
|
+
search: [
|
|
22299
|
+
{ 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"] },
|
|
22300
|
+
{ 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)"] },
|
|
22301
|
+
{ 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"] },
|
|
22302
|
+
{ 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"] }
|
|
22303
|
+
]
|
|
22304
|
+
};
|
|
22305
|
+
function getTemplate(name) {
|
|
22306
|
+
return SCENARIO_TEMPLATES[name] ?? null;
|
|
22307
|
+
}
|
|
22308
|
+
function listTemplateNames() {
|
|
22309
|
+
return Object.keys(SCENARIO_TEMPLATES);
|
|
22310
|
+
}
|
|
22311
|
+
|
|
22312
|
+
// src/mcp/index.ts
|
|
22005
22313
|
init_runs();
|
|
22006
22314
|
init_results();
|
|
22007
22315
|
init_screenshots();
|
|
@@ -22994,6 +23302,334 @@ async function runApiChecksByFilter(filter) {
|
|
|
22994
23302
|
// src/mcp/index.ts
|
|
22995
23303
|
init_personas();
|
|
22996
23304
|
init_paths();
|
|
23305
|
+
|
|
23306
|
+
// src/lib/prod-debug.ts
|
|
23307
|
+
init_secrets_resolver();
|
|
23308
|
+
var UUID_RE = /\b[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}\b/i;
|
|
23309
|
+
var SENSITIVE_PARAM_RE = /token|secret|key|password|code|state|cookie|session|grant|credential|auth|jwt|access/i;
|
|
23310
|
+
var SENSITIVE_TEXT_RE = /\b(Bearer\s+[A-Za-z0-9._-]{12,}|sk-[A-Za-z0-9]{12,}|pk_[A-Za-z0-9]{12,}|eyJ[A-Za-z0-9._-]{12,})\b/g;
|
|
23311
|
+
var URL_TEXT_RE = /https?:\/\/[^\s"'<>]+/g;
|
|
23312
|
+
function safeUrl(raw) {
|
|
23313
|
+
try {
|
|
23314
|
+
const url = new URL(raw);
|
|
23315
|
+
if (url.protocol !== "http:" && url.protocol !== "https:")
|
|
23316
|
+
return null;
|
|
23317
|
+
return url;
|
|
23318
|
+
} catch {
|
|
23319
|
+
return null;
|
|
23320
|
+
}
|
|
23321
|
+
}
|
|
23322
|
+
function normalizeOrigin(raw) {
|
|
23323
|
+
const url = safeUrl(raw);
|
|
23324
|
+
if (url)
|
|
23325
|
+
return url.origin;
|
|
23326
|
+
const hostUrl = safeUrl(`https://${raw}`);
|
|
23327
|
+
return hostUrl?.origin ?? null;
|
|
23328
|
+
}
|
|
23329
|
+
function redactProdDebugText(value) {
|
|
23330
|
+
return value.replace(URL_TEXT_RE, (match) => {
|
|
23331
|
+
const url = safeUrl(match);
|
|
23332
|
+
return url ? redactUrl(url) : match;
|
|
23333
|
+
}).replace(SENSITIVE_TEXT_RE, (match) => {
|
|
23334
|
+
if (match.startsWith("Bearer "))
|
|
23335
|
+
return "Bearer [redacted]";
|
|
23336
|
+
return "[redacted]";
|
|
23337
|
+
});
|
|
23338
|
+
}
|
|
23339
|
+
function redactUrl(url) {
|
|
23340
|
+
const clone = new URL(url.toString());
|
|
23341
|
+
for (const key of Array.from(clone.searchParams.keys())) {
|
|
23342
|
+
if (SENSITIVE_PARAM_RE.test(key)) {
|
|
23343
|
+
clone.searchParams.set(key, "[redacted]");
|
|
23344
|
+
}
|
|
23345
|
+
}
|
|
23346
|
+
return clone.toString();
|
|
23347
|
+
}
|
|
23348
|
+
function redactUrlString(value) {
|
|
23349
|
+
const url = safeUrl(value);
|
|
23350
|
+
return url ? redactUrl(url) : redactProdDebugText(value);
|
|
23351
|
+
}
|
|
23352
|
+
function parseProdDebugTarget(target) {
|
|
23353
|
+
const input = target.trim();
|
|
23354
|
+
const url = safeUrl(input);
|
|
23355
|
+
if (!url) {
|
|
23356
|
+
const id = (input.match(UUID_RE)?.[0] ?? input) || null;
|
|
23357
|
+
return {
|
|
23358
|
+
url: null,
|
|
23359
|
+
origin: null,
|
|
23360
|
+
orgSlug: null,
|
|
23361
|
+
projectRef: null,
|
|
23362
|
+
sessionId: null,
|
|
23363
|
+
agentId: null,
|
|
23364
|
+
requestId: input.startsWith("req_") ? input : null,
|
|
23365
|
+
rawId: id
|
|
23366
|
+
};
|
|
23367
|
+
}
|
|
23368
|
+
const parts = url.pathname.split("/").filter(Boolean);
|
|
23369
|
+
const projectsIndex = parts.indexOf("projects");
|
|
23370
|
+
const sessionsIndex = parts.indexOf("sessions");
|
|
23371
|
+
const orgSlug = projectsIndex > 0 ? parts[0] ?? null : null;
|
|
23372
|
+
const projectRef = projectsIndex >= 0 ? parts[projectsIndex + 1] ?? null : null;
|
|
23373
|
+
const sessionId = url.searchParams.get("session") ?? (sessionsIndex >= 0 ? parts[sessionsIndex + 1] ?? null : null);
|
|
23374
|
+
return {
|
|
23375
|
+
url: redactUrl(url),
|
|
23376
|
+
origin: url.origin,
|
|
23377
|
+
orgSlug,
|
|
23378
|
+
projectRef,
|
|
23379
|
+
sessionId,
|
|
23380
|
+
agentId: url.searchParams.get("agent"),
|
|
23381
|
+
requestId: url.searchParams.get("requestId") ?? url.searchParams.get("request_id"),
|
|
23382
|
+
rawId: input.match(UUID_RE)?.[0] ?? null
|
|
23383
|
+
};
|
|
23384
|
+
}
|
|
23385
|
+
function boundedTtl(value) {
|
|
23386
|
+
if (!Number.isFinite(value))
|
|
23387
|
+
return 15;
|
|
23388
|
+
return Math.min(Math.max(Math.round(value ?? 15), 1), 60);
|
|
23389
|
+
}
|
|
23390
|
+
function makeCommand(command) {
|
|
23391
|
+
return command.replace(/\s+/g, " ").trim();
|
|
23392
|
+
}
|
|
23393
|
+
function hostnameFromOrigin(origin) {
|
|
23394
|
+
if (!origin)
|
|
23395
|
+
return null;
|
|
23396
|
+
return safeUrl(origin)?.hostname ?? null;
|
|
23397
|
+
}
|
|
23398
|
+
function originMatches(pattern, origin) {
|
|
23399
|
+
if (!origin)
|
|
23400
|
+
return false;
|
|
23401
|
+
const normalizedPattern = normalizeOrigin(pattern);
|
|
23402
|
+
const normalizedOrigin = normalizeOrigin(origin);
|
|
23403
|
+
if (!normalizedOrigin)
|
|
23404
|
+
return false;
|
|
23405
|
+
if (normalizedPattern === normalizedOrigin)
|
|
23406
|
+
return true;
|
|
23407
|
+
const targetHost = hostnameFromOrigin(normalizedOrigin);
|
|
23408
|
+
const patternHost = normalizedPattern ? hostnameFromOrigin(normalizedPattern) : pattern.replace(/^https?:\/\//, "");
|
|
23409
|
+
if (!targetHost || !patternHost)
|
|
23410
|
+
return false;
|
|
23411
|
+
if (patternHost.startsWith("*.")) {
|
|
23412
|
+
const suffix = patternHost.slice(1);
|
|
23413
|
+
return targetHost.endsWith(suffix);
|
|
23414
|
+
}
|
|
23415
|
+
return targetHost === patternHost;
|
|
23416
|
+
}
|
|
23417
|
+
function resolveProfile(input, target, config) {
|
|
23418
|
+
const apps = config?.apps ?? {};
|
|
23419
|
+
const explicitKey = input.profile?.trim() || input.app?.trim() || config?.defaultProfile;
|
|
23420
|
+
if (explicitKey && apps[explicitKey]) {
|
|
23421
|
+
return {
|
|
23422
|
+
key: explicitKey,
|
|
23423
|
+
profile: apps[explicitKey],
|
|
23424
|
+
matchedOrigin: target.origin
|
|
23425
|
+
};
|
|
23426
|
+
}
|
|
23427
|
+
for (const [key, profile] of Object.entries(apps)) {
|
|
23428
|
+
const match = profile.origins?.find((origin) => originMatches(origin, target.origin));
|
|
23429
|
+
if (match) {
|
|
23430
|
+
return { key, profile, matchedOrigin: match };
|
|
23431
|
+
}
|
|
23432
|
+
}
|
|
23433
|
+
return { key: null, profile: null, matchedOrigin: null };
|
|
23434
|
+
}
|
|
23435
|
+
function firstResolvedCredential(...values) {
|
|
23436
|
+
for (const value of values) {
|
|
23437
|
+
if (!value?.trim())
|
|
23438
|
+
continue;
|
|
23439
|
+
const resolved = resolveCredential(value);
|
|
23440
|
+
if (resolved)
|
|
23441
|
+
return resolved;
|
|
23442
|
+
}
|
|
23443
|
+
return null;
|
|
23444
|
+
}
|
|
23445
|
+
function displayCredential(value, source) {
|
|
23446
|
+
if (!value)
|
|
23447
|
+
return null;
|
|
23448
|
+
if (source && isCredentialReference(source))
|
|
23449
|
+
return "[configured]";
|
|
23450
|
+
return redactProdDebugText(value);
|
|
23451
|
+
}
|
|
23452
|
+
function replacementValues(target, input, supportGrant) {
|
|
23453
|
+
const values = {
|
|
23454
|
+
targetUrl: target.url ?? input.target,
|
|
23455
|
+
origin: target.origin ?? "",
|
|
23456
|
+
org: target.orgSlug ?? "",
|
|
23457
|
+
project: target.projectRef ?? "",
|
|
23458
|
+
session: target.sessionId ?? "",
|
|
23459
|
+
agent: target.agentId ?? "",
|
|
23460
|
+
request: target.requestId ?? "",
|
|
23461
|
+
rawId: target.rawId ?? "",
|
|
23462
|
+
reason: input.reason ?? "",
|
|
23463
|
+
supportGrant: supportGrant ?? ""
|
|
23464
|
+
};
|
|
23465
|
+
for (const [key, value] of Object.entries({ ...values })) {
|
|
23466
|
+
values[`${key}Encoded`] = encodeURIComponent(value);
|
|
23467
|
+
}
|
|
23468
|
+
return values;
|
|
23469
|
+
}
|
|
23470
|
+
function renderTemplate(template, values) {
|
|
23471
|
+
return template.replace(/\{([a-zA-Z0-9_]+)\}/g, (_match, key) => values[key] ?? "");
|
|
23472
|
+
}
|
|
23473
|
+
function resolveSupportGrant(input, profile) {
|
|
23474
|
+
if (input.supportGrantId?.trim()) {
|
|
23475
|
+
return {
|
|
23476
|
+
value: input.supportGrantId.trim(),
|
|
23477
|
+
display: displayCredential(input.supportGrantId.trim()),
|
|
23478
|
+
source: "input"
|
|
23479
|
+
};
|
|
23480
|
+
}
|
|
23481
|
+
const source = profile?.supportGrantRef ?? profile?.supportGrantId ?? null;
|
|
23482
|
+
const value = firstResolvedCredential(profile?.supportGrantRef, profile?.supportGrantId);
|
|
23483
|
+
return { value, display: displayCredential(value, source ?? undefined), source };
|
|
23484
|
+
}
|
|
23485
|
+
function resolveSupportUrl(input, target, profile, supportGrant) {
|
|
23486
|
+
if (input.supportUrl?.trim())
|
|
23487
|
+
return input.supportUrl.trim();
|
|
23488
|
+
const direct = firstResolvedCredential(profile?.supportUrlRef, profile?.supportUrl);
|
|
23489
|
+
if (direct)
|
|
23490
|
+
return direct;
|
|
23491
|
+
if (profile?.supportUrlTemplate) {
|
|
23492
|
+
const rendered = renderTemplate(profile.supportUrlTemplate, replacementValues(target, input, supportGrant)).trim();
|
|
23493
|
+
return rendered || null;
|
|
23494
|
+
}
|
|
23495
|
+
return null;
|
|
23496
|
+
}
|
|
23497
|
+
function resolvePiiOrigin(profile, target) {
|
|
23498
|
+
if (!profile?.piiOrigin)
|
|
23499
|
+
return target.origin;
|
|
23500
|
+
return redactUrlString(renderTemplate(profile.piiOrigin, replacementValues(target, { target: target.url ?? "" }, null)));
|
|
23501
|
+
}
|
|
23502
|
+
function resolveSupportRunTarget(supportUrl, input, target) {
|
|
23503
|
+
if (supportUrl)
|
|
23504
|
+
return redactUrlString(supportUrl);
|
|
23505
|
+
return target.url ?? target.origin ?? redactProdDebugText(input.target);
|
|
23506
|
+
}
|
|
23507
|
+
function supportScenarioDescription(reason) {
|
|
23508
|
+
return `Prod debug: ${reason}. Reproduce the user-visible issue, capture console and network errors, and do not enter secrets.`;
|
|
23509
|
+
}
|
|
23510
|
+
function configuredMissing(profile, supportUrl, supportGrant, includeLogs) {
|
|
23511
|
+
const missing = [];
|
|
23512
|
+
if (!profile) {
|
|
23513
|
+
missing.push("optional: add prodDebug.apps.<profile>.origins to match this app automatically");
|
|
23514
|
+
}
|
|
23515
|
+
if (!supportUrl) {
|
|
23516
|
+
missing.push("supportUrl/supportUrlRef/supportUrlTemplate for scoped browser debugging");
|
|
23517
|
+
}
|
|
23518
|
+
if (!supportGrant) {
|
|
23519
|
+
missing.push("supportGrantId/supportGrantRef for auditable support access");
|
|
23520
|
+
}
|
|
23521
|
+
if (includeLogs && !profile?.logCommand) {
|
|
23522
|
+
missing.push("logCommand for sanitized app/provider log lookup");
|
|
23523
|
+
}
|
|
23524
|
+
return missing;
|
|
23525
|
+
}
|
|
23526
|
+
function createProdDebugPlan(input, config) {
|
|
23527
|
+
const target = parseProdDebugTarget(input.target);
|
|
23528
|
+
const browserRequested = input.includeBrowser !== false;
|
|
23529
|
+
const resolvedProfile = resolveProfile(input, target, config);
|
|
23530
|
+
const supportGrant = resolveSupportGrant(input, resolvedProfile.profile);
|
|
23531
|
+
const supportUrl = resolveSupportUrl(input, target, resolvedProfile.profile, supportGrant.value);
|
|
23532
|
+
const supportBrowserReady = Boolean(supportUrl);
|
|
23533
|
+
const app = input.app?.trim() || resolvedProfile.profile?.name || resolvedProfile.key || (target.origin ? new URL(target.origin).hostname : "app");
|
|
23534
|
+
const reason = input.reason?.trim() || "production debug requested";
|
|
23535
|
+
const actor = input.actor?.trim() || process.env["USER"] || "agent";
|
|
23536
|
+
const ttlMinutes = boundedTtl(input.ttlMinutes);
|
|
23537
|
+
const piiOrigin = resolvePiiOrigin(resolvedProfile.profile, target);
|
|
23538
|
+
const logCommand = resolvedProfile.profile?.logCommand ? redactUrlString(renderTemplate(resolvedProfile.profile.logCommand, replacementValues(target, { ...input, reason }, supportGrant.value))) : null;
|
|
23539
|
+
const safety = [
|
|
23540
|
+
"read-only by default",
|
|
23541
|
+
"no customer passwords or raw cookies",
|
|
23542
|
+
"redact tokens, OAuth codes, session values, support grants, and secrets",
|
|
23543
|
+
"verify org/user/session scope before reading data",
|
|
23544
|
+
"require explicit approval for production writes",
|
|
23545
|
+
`support access TTL capped at ${ttlMinutes} minutes`
|
|
23546
|
+
];
|
|
23547
|
+
const checks = [];
|
|
23548
|
+
const blocked = [];
|
|
23549
|
+
if (target.url) {
|
|
23550
|
+
checks.push({
|
|
23551
|
+
id: "public-route-smoke",
|
|
23552
|
+
status: "ready",
|
|
23553
|
+
description: "Open the supplied production URL and capture console/network errors without credentials.",
|
|
23554
|
+
command: makeCommand(`testers scan all ${JSON.stringify(target.url)} --json`)
|
|
23555
|
+
});
|
|
23556
|
+
}
|
|
23557
|
+
checks.push({
|
|
23558
|
+
id: "pii-redaction-scan",
|
|
23559
|
+
status: piiOrigin ? "ready" : "blocked",
|
|
23560
|
+
description: "Scan public/API responses for accidental sensitive data leakage.",
|
|
23561
|
+
command: piiOrigin ? makeCommand(`testers scan pii ${JSON.stringify(piiOrigin)} --json`) : undefined,
|
|
23562
|
+
reason: piiOrigin ? undefined : "Need a URL origin or prodDebug app profile piiOrigin to run the PII scan."
|
|
23563
|
+
});
|
|
23564
|
+
if (browserRequested) {
|
|
23565
|
+
if (supportBrowserReady) {
|
|
23566
|
+
checks.push({
|
|
23567
|
+
id: "support-browser-repro",
|
|
23568
|
+
status: "ready",
|
|
23569
|
+
description: "Use an audited support browser/session URL to reproduce the user-visible issue.",
|
|
23570
|
+
command: makeCommand(`testers run ${JSON.stringify(resolveSupportRunTarget(supportUrl, input, target))} ${JSON.stringify(supportScenarioDescription(reason))} --headed --json --overall-timeout 600000`)
|
|
23571
|
+
});
|
|
23572
|
+
} else {
|
|
23573
|
+
const reasonText = supportGrant.value ? "An audited support grant was supplied, but open-testers still needs supportUrl/supportUrlRef/supportUrlTemplate or an app adapter to open a scoped browser session." : "No audited support browser/session grant was supplied. Do not use customer passwords, copied cookies, bearer tokens, or magic links.";
|
|
23574
|
+
blocked.push(reasonText);
|
|
23575
|
+
checks.push({
|
|
23576
|
+
id: "support-browser-repro",
|
|
23577
|
+
status: "blocked",
|
|
23578
|
+
description: "Browser reproduction as the target user requires a short-lived audited support session.",
|
|
23579
|
+
reason: reasonText
|
|
23580
|
+
});
|
|
23581
|
+
}
|
|
23582
|
+
}
|
|
23583
|
+
if (input.includeLogs) {
|
|
23584
|
+
if (logCommand) {
|
|
23585
|
+
checks.push({
|
|
23586
|
+
id: "log-timeline",
|
|
23587
|
+
status: "ready",
|
|
23588
|
+
description: "Read sanitized app/provider logs by request ID, session ID, project ID, or support access ID.",
|
|
23589
|
+
command: makeCommand(logCommand)
|
|
23590
|
+
});
|
|
23591
|
+
} else {
|
|
23592
|
+
checks.push({
|
|
23593
|
+
id: "log-timeline",
|
|
23594
|
+
status: "blocked",
|
|
23595
|
+
description: "Read sanitized app/provider logs by request ID, session ID, project ID, or support access ID.",
|
|
23596
|
+
reason: "Configure prodDebug.apps.<profile>.logCommand or use an app-specific log MCP. Do not paste raw provider logs with headers/secrets."
|
|
23597
|
+
});
|
|
23598
|
+
}
|
|
23599
|
+
}
|
|
23600
|
+
if (input.allowWrites) {
|
|
23601
|
+
blocked.push("Production writes are not part of prod-debug. Require a separate explicit approval and app-specific write tool.");
|
|
23602
|
+
}
|
|
23603
|
+
return {
|
|
23604
|
+
target,
|
|
23605
|
+
app,
|
|
23606
|
+
actor,
|
|
23607
|
+
reason,
|
|
23608
|
+
ttlMinutes,
|
|
23609
|
+
setup: {
|
|
23610
|
+
profile: resolvedProfile.key,
|
|
23611
|
+
matchedOrigin: resolvedProfile.matchedOrigin,
|
|
23612
|
+
configured: {
|
|
23613
|
+
supportUrl: Boolean(supportUrl),
|
|
23614
|
+
supportGrant: Boolean(supportGrant.value),
|
|
23615
|
+
piiOrigin: Boolean(piiOrigin),
|
|
23616
|
+
logCommand: Boolean(logCommand)
|
|
23617
|
+
},
|
|
23618
|
+
missing: configuredMissing(resolvedProfile.profile, supportUrl, supportGrant.value, Boolean(input.includeLogs))
|
|
23619
|
+
},
|
|
23620
|
+
supportAccess: {
|
|
23621
|
+
required: browserRequested,
|
|
23622
|
+
grantId: supportGrant.display,
|
|
23623
|
+
browserReady: supportBrowserReady,
|
|
23624
|
+
note: supportBrowserReady ? "Use the provided audited support access; never print token/cookie values." : "Configure an audited support-browser/session URL, URL ref, or template before user-scoped browser debugging."
|
|
23625
|
+
},
|
|
23626
|
+
safety,
|
|
23627
|
+
checks,
|
|
23628
|
+
blocked
|
|
23629
|
+
};
|
|
23630
|
+
}
|
|
23631
|
+
|
|
23632
|
+
// src/mcp/index.ts
|
|
22997
23633
|
var cliArgs = new Set(process.argv.slice(2));
|
|
22998
23634
|
if (cliArgs.has("--help") || cliArgs.has("-h")) {
|
|
22999
23635
|
console.log(`Usage: testers-mcp [options]
|
|
@@ -23070,6 +23706,26 @@ var server = new McpServer({
|
|
|
23070
23706
|
name: "testers",
|
|
23071
23707
|
version: "0.0.1"
|
|
23072
23708
|
});
|
|
23709
|
+
server.tool("create_prod_debug_plan", "Create a safe production debug plan for a URL, session ID, project ID, user report, or request ID. Does not use passwords or raw cookies; browser debugging is blocked unless an audited support URL is supplied or an app adapter can resolve a support grant.", {
|
|
23710
|
+
target: exports_external.string().describe("Production URL, session ID, project ID, request ID, or other target evidence"),
|
|
23711
|
+
app: exports_external.string().optional().describe("App name for reporting"),
|
|
23712
|
+
profile: exports_external.string().optional().describe("prodDebug app profile from testers config"),
|
|
23713
|
+
actor: exports_external.string().optional().describe("Operator/agent identity for audit context"),
|
|
23714
|
+
reason: exports_external.string().optional().describe("Debug reason or support context"),
|
|
23715
|
+
supportUrl: exports_external.string().optional().describe("Audited support browser/session URL minted by the target app"),
|
|
23716
|
+
supportGrantId: exports_external.string().optional().describe("Audited support access grant ID"),
|
|
23717
|
+
ttlMinutes: exports_external.number().optional().describe("Support access TTL in minutes, capped at 60"),
|
|
23718
|
+
includeBrowser: exports_external.boolean().optional().describe("Include user-scoped browser reproduction check"),
|
|
23719
|
+
includeLogs: exports_external.boolean().optional().describe("Include log timeline adapter requirement"),
|
|
23720
|
+
allowWrites: exports_external.boolean().optional().describe("Document that writes require a separate explicit approval")
|
|
23721
|
+
}, async (input) => {
|
|
23722
|
+
try {
|
|
23723
|
+
const config = loadConfig();
|
|
23724
|
+
return json(createProdDebugPlan(input, config.prodDebug));
|
|
23725
|
+
} catch (error) {
|
|
23726
|
+
return errorResponse(error);
|
|
23727
|
+
}
|
|
23728
|
+
});
|
|
23073
23729
|
server.tool("create_scenario", "Create a new test scenario", {
|
|
23074
23730
|
name: exports_external.string().describe("Scenario name"),
|
|
23075
23731
|
description: exports_external.string().describe("What this scenario tests"),
|
|
@@ -23078,15 +23734,87 @@ server.tool("create_scenario", "Create a new test scenario", {
|
|
|
23078
23734
|
priority: exports_external.enum(["low", "medium", "high", "critical"]).optional().describe("Scenario priority"),
|
|
23079
23735
|
model: exports_external.string().optional().describe(MODEL_DESC),
|
|
23080
23736
|
targetPath: exports_external.string().optional().describe("URL path to navigate to"),
|
|
23081
|
-
requiresAuth: exports_external.boolean().optional().describe("Whether scenario requires authentication")
|
|
23082
|
-
|
|
23737
|
+
requiresAuth: exports_external.boolean().optional().describe("Whether scenario requires authentication"),
|
|
23738
|
+
projectId: exports_external.string().optional().describe("Project ID to scope this scenario to")
|
|
23739
|
+
}, async ({ name, description, steps, tags, priority, model, targetPath, requiresAuth, projectId }) => {
|
|
23083
23740
|
try {
|
|
23084
|
-
const scenario = createScenario({ name, description, steps, tags, priority, model, targetPath, requiresAuth });
|
|
23741
|
+
const scenario = createScenario({ name, description, steps, tags, priority, model, targetPath, requiresAuth, projectId });
|
|
23085
23742
|
return json(scenario);
|
|
23086
23743
|
} catch (error) {
|
|
23087
23744
|
return errorResponse(error);
|
|
23088
23745
|
}
|
|
23089
23746
|
});
|
|
23747
|
+
server.tool("batch_create_scenarios", "Create multiple test scenarios in a single call. Each item requires name; description defaults to the name.", {
|
|
23748
|
+
scenarios: exports_external.array(exports_external.object({
|
|
23749
|
+
name: exports_external.string().describe("Scenario name"),
|
|
23750
|
+
description: exports_external.string().optional().describe("What this scenario tests"),
|
|
23751
|
+
steps: exports_external.array(exports_external.string()).optional().describe("Ordered test steps"),
|
|
23752
|
+
tags: exports_external.array(exports_external.string()).optional().describe("Tags for filtering"),
|
|
23753
|
+
priority: exports_external.enum(["low", "medium", "high", "critical"]).optional().describe("Scenario priority"),
|
|
23754
|
+
model: exports_external.string().optional().describe(MODEL_DESC),
|
|
23755
|
+
targetPath: exports_external.string().optional().describe("URL path to navigate to"),
|
|
23756
|
+
url: exports_external.string().optional().describe("Alias for targetPath"),
|
|
23757
|
+
requiresAuth: exports_external.boolean().optional().describe("Whether scenario requires authentication")
|
|
23758
|
+
})).min(1).max(100).describe("Array of scenarios to create"),
|
|
23759
|
+
projectId: exports_external.string().optional().describe("Project ID to scope all scenarios to")
|
|
23760
|
+
}, async ({ scenarios, projectId }) => {
|
|
23761
|
+
try {
|
|
23762
|
+
const results = [];
|
|
23763
|
+
for (const s of scenarios) {
|
|
23764
|
+
try {
|
|
23765
|
+
const scenario = createScenario({
|
|
23766
|
+
...s,
|
|
23767
|
+
description: s.description ?? s.name,
|
|
23768
|
+
targetPath: s.targetPath ?? s.url,
|
|
23769
|
+
projectId
|
|
23770
|
+
});
|
|
23771
|
+
results.push({ id: scenario.id, name: scenario.name, shortId: scenario.shortId });
|
|
23772
|
+
} catch (e) {
|
|
23773
|
+
results.push({ id: "", name: s.name, shortId: "", error: e instanceof Error ? e.message : String(e) });
|
|
23774
|
+
}
|
|
23775
|
+
}
|
|
23776
|
+
const created = results.filter((r) => !r.error).length;
|
|
23777
|
+
const failed = results.filter((r) => r.error).length;
|
|
23778
|
+
return json({ created, failed, total: scenarios.length, results });
|
|
23779
|
+
} catch (error) {
|
|
23780
|
+
return errorResponse(error);
|
|
23781
|
+
}
|
|
23782
|
+
});
|
|
23783
|
+
server.tool("list_templates", "List built-in scenario templates available for quick setup. Use apply_template to create scenarios from a template.", {}, async () => {
|
|
23784
|
+
try {
|
|
23785
|
+
const templates = listTemplateNames().map((name) => {
|
|
23786
|
+
const scenarios = getTemplate(name);
|
|
23787
|
+
return { name, scenarioCount: scenarios.length, scenarios: scenarios.map((s) => ({ name: s.name, description: s.description, priority: s.priority, tags: s.tags })) };
|
|
23788
|
+
});
|
|
23789
|
+
return json({ templates });
|
|
23790
|
+
} catch (error) {
|
|
23791
|
+
return errorResponse(error);
|
|
23792
|
+
}
|
|
23793
|
+
});
|
|
23794
|
+
server.tool("apply_template", "Create scenarios from a built-in template. Returns the created scenario IDs and any errors.", {
|
|
23795
|
+
template: exports_external.string().describe("Template name to apply (auth, crud, forms, nav, a11y, checkout, search)"),
|
|
23796
|
+
projectId: exports_external.string().optional().describe("Project ID to scope scenarios to")
|
|
23797
|
+
}, async ({ template, projectId }) => {
|
|
23798
|
+
try {
|
|
23799
|
+
const scenarios = getTemplate(template);
|
|
23800
|
+
if (!scenarios)
|
|
23801
|
+
return errorResponse(new Error(`Template not found: ${template}. Available: ${listTemplateNames().join(", ")}`));
|
|
23802
|
+
const results = [];
|
|
23803
|
+
for (const s of scenarios) {
|
|
23804
|
+
try {
|
|
23805
|
+
const scenario = createScenario({ ...s, projectId });
|
|
23806
|
+
results.push({ id: scenario.id, name: scenario.name, shortId: scenario.shortId });
|
|
23807
|
+
} catch (e) {
|
|
23808
|
+
results.push({ id: "", name: s.name, shortId: "", error: e instanceof Error ? e.message : String(e) });
|
|
23809
|
+
}
|
|
23810
|
+
}
|
|
23811
|
+
const created = results.filter((r) => !r.error).length;
|
|
23812
|
+
const failed = results.filter((r) => r.error).length;
|
|
23813
|
+
return json({ template, created, failed, total: scenarios.length, results });
|
|
23814
|
+
} catch (error) {
|
|
23815
|
+
return errorResponse(error);
|
|
23816
|
+
}
|
|
23817
|
+
});
|
|
23090
23818
|
server.tool("get_scenario", `Get a scenario by ID or short ID. ${ID_DESC}`, {
|
|
23091
23819
|
id: exports_external.string().describe(`Scenario ID or short ID. ${ID_DESC}`)
|
|
23092
23820
|
}, async ({ id }) => {
|
|
@@ -23157,12 +23885,15 @@ server.tool("run_scenarios", "Run test scenarios against a URL. Provide url dire
|
|
|
23157
23885
|
headed: exports_external.boolean().optional().describe("Run browser in headed mode"),
|
|
23158
23886
|
parallel: exports_external.number().optional().describe("Number of parallel workers"),
|
|
23159
23887
|
personaId: exports_external.string().optional().describe("Override persona ID for this run"),
|
|
23888
|
+
personaIds: exports_external.array(exports_external.string()).optional().describe("Run with multiple personas for divergence testing (each persona runs all scenarios)"),
|
|
23160
23889
|
samples: exports_external.number().int().min(1).max(20).optional().describe("Run each scenario N times for flakiness detection (default 1)"),
|
|
23161
23890
|
flakinessThreshold: exports_external.number().min(0).max(1).optional().describe("Pass rate below which scenario is marked flaky (default 0.95)"),
|
|
23162
23891
|
maxCostCents: exports_external.number().optional().describe("Hard budget cap in cents \u2014 run is rejected before starting if estimated cost exceeds this"),
|
|
23163
23892
|
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
|
-
|
|
23893
|
+
minimal: exports_external.boolean().optional().describe("Fastest mode: cheapest model, max parallelism, min turns \u2014 ideal for CI smoke checks"),
|
|
23894
|
+
timeoutMs: exports_external.number().optional().describe("Per-scenario timeout in ms (default 120000)"),
|
|
23895
|
+
recordVideo: exports_external.boolean().optional().describe("Record video of each scenario run (Playwright only)")
|
|
23896
|
+
}, async ({ url, env: env2, tags, scenarioIds, priority, model, headed, parallel, personaId, personaIds, samples, flakinessThreshold, maxCostCents, cacheMaxAgeMs, minimal, timeoutMs, recordVideo }) => {
|
|
23166
23897
|
try {
|
|
23167
23898
|
let resolvedUrl = url;
|
|
23168
23899
|
if (!resolvedUrl && env2) {
|
|
@@ -23180,12 +23911,54 @@ server.tool("run_scenarios", "Run test scenarios against a URL. Provide url dire
|
|
|
23180
23911
|
}
|
|
23181
23912
|
if (!resolvedUrl)
|
|
23182
23913
|
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 });
|
|
23914
|
+
const { runId, scenarioCount } = startRunAsync({ url: resolvedUrl, tags, scenarioIds, priority, model, headed, parallel, personaId, personaIds, samples, flakinessThreshold, maxCostCents, cacheMaxAgeMs, minimal, timeout: timeoutMs, recordVideo });
|
|
23184
23915
|
return json({ runId, scenarioCount, url: resolvedUrl, status: "running", message: "Poll with get_run to check progress." });
|
|
23185
23916
|
} catch (error) {
|
|
23186
23917
|
return errorResponse(error);
|
|
23187
23918
|
}
|
|
23188
23919
|
});
|
|
23920
|
+
server.tool("retry_failed", "Re-run only failed/errored scenarios from a previous run. Creates a new run with only the failing scenarios.", {
|
|
23921
|
+
runId: exports_external.string().describe("Previous run ID to retry failures from"),
|
|
23922
|
+
url: exports_external.string().optional().describe("Target URL (overrides original run URL)"),
|
|
23923
|
+
model: exports_external.string().optional().describe(MODEL_DESC),
|
|
23924
|
+
headed: exports_external.boolean().optional().describe("Run browser in headed mode"),
|
|
23925
|
+
parallel: exports_external.number().optional().describe("Number of parallel workers"),
|
|
23926
|
+
maxRetries: exports_external.number().int().min(0).max(3).optional().describe("Max retries per failed scenario"),
|
|
23927
|
+
maxCostCents: exports_external.number().optional().describe("Hard budget cap in cents")
|
|
23928
|
+
}, async ({ runId, url, model, headed, parallel, maxRetries, maxCostCents }) => {
|
|
23929
|
+
try {
|
|
23930
|
+
const run = getRun(runId);
|
|
23931
|
+
if (!run)
|
|
23932
|
+
return errorResponse(notFoundErr(runId, "Run"));
|
|
23933
|
+
if (run.status !== "failed")
|
|
23934
|
+
return errorResponse(new Error("Run is not in failed state. Can only retry failures from a failed run."));
|
|
23935
|
+
const results = getResultsByRun(runId);
|
|
23936
|
+
const failedResultIds = results.filter((r) => r.status === "failed" || r.status === "error").map((r) => r.scenarioId);
|
|
23937
|
+
if (failedResultIds.length === 0)
|
|
23938
|
+
return errorResponse(new Error("No failed results found in this run."));
|
|
23939
|
+
const resolvedUrl = url ?? run.url;
|
|
23940
|
+
const { runId: newRunId, scenarioCount } = startRunAsync({
|
|
23941
|
+
url: resolvedUrl,
|
|
23942
|
+
scenarioIds: failedResultIds,
|
|
23943
|
+
model,
|
|
23944
|
+
headed,
|
|
23945
|
+
parallel,
|
|
23946
|
+
retry: maxRetries ?? 0,
|
|
23947
|
+
maxCostCents
|
|
23948
|
+
});
|
|
23949
|
+
return json({
|
|
23950
|
+
runId: newRunId,
|
|
23951
|
+
originalRunId: runId,
|
|
23952
|
+
scenarioCount,
|
|
23953
|
+
retriedScenarioIds: failedResultIds,
|
|
23954
|
+
url: resolvedUrl,
|
|
23955
|
+
status: "running",
|
|
23956
|
+
message: "Poll with get_run to check progress."
|
|
23957
|
+
});
|
|
23958
|
+
} catch (error) {
|
|
23959
|
+
return errorResponse(error);
|
|
23960
|
+
}
|
|
23961
|
+
});
|
|
23189
23962
|
server.tool("get_run", `Get details of a test run. ${ID_DESC}`, {
|
|
23190
23963
|
id: exports_external.string().describe(`Run ID. ${ID_DESC}`)
|
|
23191
23964
|
}, async ({ id }) => {
|
|
@@ -23199,11 +23972,17 @@ server.tool("get_run", `Get details of a test run. ${ID_DESC}`, {
|
|
|
23199
23972
|
}
|
|
23200
23973
|
});
|
|
23201
23974
|
server.tool("list_runs", "List test runs with optional filters", {
|
|
23975
|
+
projectId: exports_external.string().optional().describe("Filter by project ID"),
|
|
23202
23976
|
status: exports_external.enum(["pending", "running", "passed", "failed", "cancelled"]).optional().describe("Filter by status"),
|
|
23203
|
-
|
|
23204
|
-
|
|
23977
|
+
since: exports_external.string().optional().describe("Filter runs started at or after this ISO date"),
|
|
23978
|
+
until: exports_external.string().optional().describe("Filter runs started at or before this ISO date"),
|
|
23979
|
+
sort: exports_external.enum(["date", "duration", "cost"]).optional().describe("Sort field"),
|
|
23980
|
+
desc: exports_external.boolean().optional().describe("Sort descending (default true)"),
|
|
23981
|
+
limit: exports_external.number().optional().describe("Max results to return"),
|
|
23982
|
+
offset: exports_external.number().optional().describe("Number of results to skip")
|
|
23983
|
+
}, async ({ projectId, status, since, until, sort, desc, limit, offset }) => {
|
|
23205
23984
|
try {
|
|
23206
|
-
const runs = listRuns({ status, limit });
|
|
23985
|
+
const runs = listRuns({ projectId, status, since, until, sort, desc, limit, offset });
|
|
23207
23986
|
return json({ items: runs, total: runs.length });
|
|
23208
23987
|
} catch (error) {
|
|
23209
23988
|
return errorResponse(error);
|
|
@@ -23372,41 +24151,6 @@ server.tool("get_run_costs", "Get cost breakdown for a run, with per-scenario de
|
|
|
23372
24151
|
return errorResponse(error);
|
|
23373
24152
|
}
|
|
23374
24153
|
});
|
|
23375
|
-
server.tool("batch_create_scenarios", "Create multiple scenarios in a single call. Returns created scenarios and any failures.", {
|
|
23376
|
-
scenarios: exports_external.array(exports_external.object({
|
|
23377
|
-
name: exports_external.string().describe("Scenario name"),
|
|
23378
|
-
url: exports_external.string().optional().describe("Target URL (stored as targetPath)"),
|
|
23379
|
-
description: exports_external.string().optional().describe("What this scenario tests"),
|
|
23380
|
-
steps: exports_external.array(exports_external.string()).optional().describe("Ordered test steps"),
|
|
23381
|
-
tags: exports_external.array(exports_external.string()).optional().describe("Tags for filtering"),
|
|
23382
|
-
priority: exports_external.enum(["low", "medium", "high", "critical"]).optional().describe("Scenario priority")
|
|
23383
|
-
})).describe("Array of scenarios to create")
|
|
23384
|
-
}, async ({ scenarios }) => {
|
|
23385
|
-
const created = [];
|
|
23386
|
-
const failed = [];
|
|
23387
|
-
for (let i = 0;i < scenarios.length; i++) {
|
|
23388
|
-
const input = scenarios[i];
|
|
23389
|
-
try {
|
|
23390
|
-
const scenario = createScenario({
|
|
23391
|
-
name: input.name,
|
|
23392
|
-
description: input.description ?? input.name,
|
|
23393
|
-
steps: input.steps,
|
|
23394
|
-
tags: input.tags,
|
|
23395
|
-
priority: input.priority,
|
|
23396
|
-
targetPath: input.url
|
|
23397
|
-
});
|
|
23398
|
-
created.push(scenario);
|
|
23399
|
-
} catch (error) {
|
|
23400
|
-
const e = error instanceof Error ? error : new Error(String(error));
|
|
23401
|
-
failed.push({ index: i, name: input.name, error: e.message });
|
|
23402
|
-
}
|
|
23403
|
-
}
|
|
23404
|
-
const lines = [
|
|
23405
|
-
`Created: ${created.length} scenario(s)`,
|
|
23406
|
-
...created.map((s) => ` [${s.shortId}] ${s.name}`)
|
|
23407
|
-
];
|
|
23408
|
-
return json({ created, failed });
|
|
23409
|
-
});
|
|
23410
24154
|
server.tool("cancel_run", "Mark a run as cancelled in the database. In-flight browser processes may still complete but results will be ignored.", {
|
|
23411
24155
|
runId: exports_external.string().describe("Run ID to cancel")
|
|
23412
24156
|
}, async ({ runId }) => {
|
|
@@ -23597,14 +24341,15 @@ server.tool("run_health_scan", "Run all scanners (console, network, links, perfo
|
|
|
23597
24341
|
url: exports_external.string().describe("URL to scan"),
|
|
23598
24342
|
pages: exports_external.array(exports_external.string()).optional().describe("Specific paths to include"),
|
|
23599
24343
|
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)"),
|
|
24344
|
+
scanners: exports_external.array(exports_external.enum(["console", "network", "links", "performance", "a11y"])).optional().describe("Which scanners to run (default: console, network, links)"),
|
|
23601
24345
|
maxPages: exports_external.number().optional().describe("Max pages for link crawl (default 20)"),
|
|
24346
|
+
wcagLevel: exports_external.enum(["A", "AA", "AAA"]).optional().describe("WCAG compliance level for a11y scanner (default: AA)"),
|
|
23602
24347
|
headed: exports_external.boolean().optional(),
|
|
23603
24348
|
timeoutMs: exports_external.number().optional()
|
|
23604
|
-
}, async ({ url, pages, projectId, scanners, maxPages, headed, timeoutMs }) => {
|
|
24349
|
+
}, async ({ url, pages, projectId, scanners, maxPages, headed, timeoutMs, wcagLevel }) => {
|
|
23605
24350
|
try {
|
|
23606
24351
|
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 });
|
|
24352
|
+
const summary = await runHealthScan2({ url, pages, projectId, scanners, maxPages, headed, timeoutMs, wcagLevel });
|
|
23608
24353
|
return json(summary);
|
|
23609
24354
|
} catch (e) {
|
|
23610
24355
|
return errorResponse(e);
|
|
@@ -24362,6 +25107,32 @@ Context: ${context}` : ""}`,
|
|
|
24362
25107
|
return errorResponse(e);
|
|
24363
25108
|
}
|
|
24364
25109
|
});
|
|
25110
|
+
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}`, {
|
|
25111
|
+
resultId: exports_external.string().describe(`Result ID. ${ID_DESC}`),
|
|
25112
|
+
includeContent: exports_external.boolean().optional().describe("Return full HAR JSON content (default: false)")
|
|
25113
|
+
}, async ({ resultId, includeContent }) => {
|
|
25114
|
+
try {
|
|
25115
|
+
const { getResult: getResult2 } = await Promise.resolve().then(() => (init_results(), exports_results));
|
|
25116
|
+
const result = getResult2(resultId);
|
|
25117
|
+
if (!result)
|
|
25118
|
+
return errorResponse(notFoundErr(resultId, "Result"));
|
|
25119
|
+
const harPath = result.harPath ?? result.metadata?.harPath;
|
|
25120
|
+
if (!harPath)
|
|
25121
|
+
return json({ resultId, harAvailable: false, message: "No HAR file recorded for this result." });
|
|
25122
|
+
const harFile = Bun.file(harPath);
|
|
25123
|
+
const exists = await harFile.exists();
|
|
25124
|
+
if (!exists)
|
|
25125
|
+
return json({ resultId, harPath, harAvailable: false, message: "HAR file was recorded but has been cleaned up." });
|
|
25126
|
+
if (!includeContent) {
|
|
25127
|
+
const size = await harFile.size();
|
|
25128
|
+
return json({ resultId, harPath, harAvailable: true, sizeBytes: size, message: "HAR file available. Use includeContent: true to retrieve." });
|
|
25129
|
+
}
|
|
25130
|
+
const harContent = await harFile.text();
|
|
25131
|
+
return json({ resultId, harPath, harAvailable: true, har: JSON.parse(harContent) });
|
|
25132
|
+
} catch (e) {
|
|
25133
|
+
return errorResponse(e);
|
|
25134
|
+
}
|
|
25135
|
+
});
|
|
24365
25136
|
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
25137
|
runId: exports_external.string().describe("Run ID to wait for"),
|
|
24367
25138
|
timeoutMs: exports_external.number().optional().default(300000).describe("Max wait time in ms (default 5 minutes)"),
|
|
@@ -24644,6 +25415,13 @@ server.tool("list_scenarios_by_page", "Group scenarios by page (targetPath). Sho
|
|
|
24644
25415
|
}
|
|
24645
25416
|
});
|
|
24646
25417
|
registerCloudTools(server, "testers");
|
|
25418
|
+
process.on("unhandledRejection", (reason) => {
|
|
25419
|
+
const msg = reason instanceof Error ? reason.stack ?? reason.message : String(reason);
|
|
25420
|
+
console.error(`[testers-mcp] Unhandled promise rejection: ${msg}`);
|
|
25421
|
+
});
|
|
25422
|
+
process.on("uncaughtException", (err) => {
|
|
25423
|
+
console.error(`[testers-mcp] Uncaught exception: ${err.stack ?? err.message}`);
|
|
25424
|
+
});
|
|
24647
25425
|
async function main() {
|
|
24648
25426
|
const transport = new StdioServerTransport;
|
|
24649
25427
|
await server.connect(transport);
|