@hasna/testers 0.0.29 → 0.0.31
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/README.md +32 -0
- package/dist/cli/index.js +411 -4
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +379 -1
- package/dist/lib/a11y-audit.d.ts.map +1 -1
- package/dist/lib/api-discovery.d.ts.map +1 -1
- package/dist/lib/browser.d.ts +1 -1
- package/dist/lib/browser.d.ts.map +1 -1
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/discovery.d.ts +11 -1
- package/dist/lib/discovery.d.ts.map +1 -1
- package/dist/lib/dom-mutation.d.ts.map +1 -1
- package/dist/lib/hybrid-runner.d.ts.map +1 -1
- package/dist/lib/junit-export.d.ts.map +1 -1
- package/dist/lib/offline-mode.d.ts.map +1 -1
- package/dist/lib/preview-detect.d.ts +1 -1
- package/dist/lib/preview-detect.d.ts.map +1 -1
- package/dist/lib/prod-debug.d.ts +77 -0
- package/dist/lib/prod-debug.d.ts.map +1 -0
- package/dist/lib/responsive.d.ts.map +1 -1
- package/dist/mcp/index.js +451 -41
- package/dist/sdk/index.d.ts +3 -2
- package/dist/sdk/index.d.ts.map +1 -1
- package/dist/server/index.js +90 -4
- package/dist/types/index.d.ts +2 -0
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +2 -2
package/dist/mcp/index.js
CHANGED
|
@@ -12253,7 +12253,8 @@ function loadConfig() {
|
|
|
12253
12253
|
judgeModel: fileConfig.judgeModel,
|
|
12254
12254
|
judgeProvider: fileConfig.judgeProvider,
|
|
12255
12255
|
selfHeal: fileConfig.selfHeal ?? false,
|
|
12256
|
-
conversationsSpace: fileConfig.conversationsSpace
|
|
12256
|
+
conversationsSpace: fileConfig.conversationsSpace,
|
|
12257
|
+
prodDebug: fileConfig.prodDebug
|
|
12257
12258
|
};
|
|
12258
12259
|
const envModel = process.env["TESTERS_MODEL"];
|
|
12259
12260
|
if (envModel) {
|
|
@@ -15105,6 +15106,11 @@ function resolveCredential(value) {
|
|
|
15105
15106
|
}
|
|
15106
15107
|
return value;
|
|
15107
15108
|
}
|
|
15109
|
+
function isCredentialReference(value) {
|
|
15110
|
+
if (!value)
|
|
15111
|
+
return false;
|
|
15112
|
+
return value.startsWith("@secrets:") || value.startsWith("$");
|
|
15113
|
+
}
|
|
15108
15114
|
var init_secrets_resolver = () => {};
|
|
15109
15115
|
|
|
15110
15116
|
// src/lib/persona-auth.ts
|
|
@@ -22255,6 +22261,91 @@ var coerce = {
|
|
|
22255
22261
|
var NEVER = INVALID;
|
|
22256
22262
|
// src/mcp/index.ts
|
|
22257
22263
|
init_dist();
|
|
22264
|
+
// package.json
|
|
22265
|
+
var package_default = {
|
|
22266
|
+
name: "@hasna/testers",
|
|
22267
|
+
version: "0.0.31",
|
|
22268
|
+
description: "AI-powered QA testing CLI \u2014 spawns cheap AI agents to test web apps with headless browsers",
|
|
22269
|
+
type: "module",
|
|
22270
|
+
main: "dist/index.js",
|
|
22271
|
+
types: "dist/index.d.ts",
|
|
22272
|
+
bin: {
|
|
22273
|
+
testers: "dist/cli/index.js",
|
|
22274
|
+
"testers-mcp": "dist/mcp/index.js",
|
|
22275
|
+
"testers-serve": "dist/server/index.js"
|
|
22276
|
+
},
|
|
22277
|
+
exports: {
|
|
22278
|
+
".": {
|
|
22279
|
+
types: "./dist/index.d.ts",
|
|
22280
|
+
import: "./dist/index.js"
|
|
22281
|
+
}
|
|
22282
|
+
},
|
|
22283
|
+
files: [
|
|
22284
|
+
"dist/",
|
|
22285
|
+
"dashboard/dist/",
|
|
22286
|
+
"LICENSE",
|
|
22287
|
+
"README.md"
|
|
22288
|
+
],
|
|
22289
|
+
scripts: {
|
|
22290
|
+
build: "bun run build:dashboard && bun run build:cli && bun run build:mcp && bun run build:server && bun run build:lib && bun run build:types",
|
|
22291
|
+
"build:cli": "bun build src/cli/index.tsx --outdir dist/cli --target bun --external ink --external react --external chalk --external @modelcontextprotocol/sdk --external @anthropic-ai/sdk --external playwright --external @hasna/browser",
|
|
22292
|
+
"build:mcp": "bun build src/mcp/index.ts --outdir dist/mcp --target bun --external @modelcontextprotocol/sdk --external @anthropic-ai/sdk --external playwright --external @hasna/browser",
|
|
22293
|
+
"build:server": "bun build src/server/index.ts --outdir dist/server --target bun --external @anthropic-ai/sdk --external playwright --external @hasna/browser",
|
|
22294
|
+
"build:lib": "bun build src/index.ts --outdir dist --target bun --external playwright --external @anthropic-ai/sdk --external @modelcontextprotocol/sdk --external @hasna/browser",
|
|
22295
|
+
"build:types": "NODE_OPTIONS='--max-old-space-size=8192' tsc --emitDeclarationOnly --outDir dist --skipLibCheck",
|
|
22296
|
+
"build:dashboard": "cd dashboard && bun run build",
|
|
22297
|
+
typecheck: "tsc --noEmit",
|
|
22298
|
+
test: "bun test",
|
|
22299
|
+
"dev:cli": "bun run src/cli/index.tsx",
|
|
22300
|
+
"dev:mcp": "bun run src/mcp/index.ts",
|
|
22301
|
+
"dev:serve": "bun run src/server/index.ts",
|
|
22302
|
+
prepublishOnly: "bun run build",
|
|
22303
|
+
postinstall: "mkdir -p $HOME/.hasna/testers/screenshots $HOME/.hasna/testers/browser/profiles 2>/dev/null || true"
|
|
22304
|
+
},
|
|
22305
|
+
dependencies: {
|
|
22306
|
+
"@anthropic-ai/sdk": "^0.52.0",
|
|
22307
|
+
"@hasna/browser": "^0.3.8",
|
|
22308
|
+
"@hasna/cloud": "^0.1.24",
|
|
22309
|
+
"@hasna/contacts": "^0.6.8",
|
|
22310
|
+
"@modelcontextprotocol/sdk": "^1.12.1",
|
|
22311
|
+
chalk: "^5.4.1",
|
|
22312
|
+
commander: "^13.1.0",
|
|
22313
|
+
ink: "^5.2.0",
|
|
22314
|
+
playwright: "^1.50.0",
|
|
22315
|
+
react: "^18.3.1",
|
|
22316
|
+
zod: "^3.24.2"
|
|
22317
|
+
},
|
|
22318
|
+
devDependencies: {
|
|
22319
|
+
"@types/bun": "latest",
|
|
22320
|
+
"@types/react": "^18.3.18",
|
|
22321
|
+
typescript: "^5.7.3"
|
|
22322
|
+
},
|
|
22323
|
+
engines: {
|
|
22324
|
+
bun: ">=1.0.0"
|
|
22325
|
+
},
|
|
22326
|
+
publishConfig: {
|
|
22327
|
+
registry: "https://registry.npmjs.org",
|
|
22328
|
+
access: "public"
|
|
22329
|
+
},
|
|
22330
|
+
repository: {
|
|
22331
|
+
type: "git",
|
|
22332
|
+
url: "git+https://github.com/hasna/testers.git"
|
|
22333
|
+
},
|
|
22334
|
+
license: "Apache-2.0",
|
|
22335
|
+
keywords: [
|
|
22336
|
+
"testing",
|
|
22337
|
+
"qa",
|
|
22338
|
+
"ai",
|
|
22339
|
+
"playwright",
|
|
22340
|
+
"browser",
|
|
22341
|
+
"screenshot",
|
|
22342
|
+
"automation",
|
|
22343
|
+
"cli",
|
|
22344
|
+
"mcp"
|
|
22345
|
+
]
|
|
22346
|
+
};
|
|
22347
|
+
|
|
22348
|
+
// src/mcp/index.ts
|
|
22258
22349
|
init_scenarios();
|
|
22259
22350
|
|
|
22260
22351
|
// src/lib/templates.ts
|
|
@@ -23296,6 +23387,334 @@ async function runApiChecksByFilter(filter) {
|
|
|
23296
23387
|
// src/mcp/index.ts
|
|
23297
23388
|
init_personas();
|
|
23298
23389
|
init_paths();
|
|
23390
|
+
|
|
23391
|
+
// src/lib/prod-debug.ts
|
|
23392
|
+
init_secrets_resolver();
|
|
23393
|
+
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;
|
|
23394
|
+
var SENSITIVE_PARAM_RE = /token|secret|key|password|code|state|cookie|session|grant|credential|auth|jwt|access/i;
|
|
23395
|
+
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;
|
|
23396
|
+
var URL_TEXT_RE = /https?:\/\/[^\s"'<>]+/g;
|
|
23397
|
+
function safeUrl(raw) {
|
|
23398
|
+
try {
|
|
23399
|
+
const url = new URL(raw);
|
|
23400
|
+
if (url.protocol !== "http:" && url.protocol !== "https:")
|
|
23401
|
+
return null;
|
|
23402
|
+
return url;
|
|
23403
|
+
} catch {
|
|
23404
|
+
return null;
|
|
23405
|
+
}
|
|
23406
|
+
}
|
|
23407
|
+
function normalizeOrigin(raw) {
|
|
23408
|
+
const url = safeUrl(raw);
|
|
23409
|
+
if (url)
|
|
23410
|
+
return url.origin;
|
|
23411
|
+
const hostUrl = safeUrl(`https://${raw}`);
|
|
23412
|
+
return hostUrl?.origin ?? null;
|
|
23413
|
+
}
|
|
23414
|
+
function redactProdDebugText(value) {
|
|
23415
|
+
return value.replace(URL_TEXT_RE, (match) => {
|
|
23416
|
+
const url = safeUrl(match);
|
|
23417
|
+
return url ? redactUrl(url) : match;
|
|
23418
|
+
}).replace(SENSITIVE_TEXT_RE, (match) => {
|
|
23419
|
+
if (match.startsWith("Bearer "))
|
|
23420
|
+
return "Bearer [redacted]";
|
|
23421
|
+
return "[redacted]";
|
|
23422
|
+
});
|
|
23423
|
+
}
|
|
23424
|
+
function redactUrl(url) {
|
|
23425
|
+
const clone = new URL(url.toString());
|
|
23426
|
+
for (const key of Array.from(clone.searchParams.keys())) {
|
|
23427
|
+
if (SENSITIVE_PARAM_RE.test(key)) {
|
|
23428
|
+
clone.searchParams.set(key, "[redacted]");
|
|
23429
|
+
}
|
|
23430
|
+
}
|
|
23431
|
+
return clone.toString();
|
|
23432
|
+
}
|
|
23433
|
+
function redactUrlString(value) {
|
|
23434
|
+
const url = safeUrl(value);
|
|
23435
|
+
return url ? redactUrl(url) : redactProdDebugText(value);
|
|
23436
|
+
}
|
|
23437
|
+
function parseProdDebugTarget(target) {
|
|
23438
|
+
const input = target.trim();
|
|
23439
|
+
const url = safeUrl(input);
|
|
23440
|
+
if (!url) {
|
|
23441
|
+
const id = (input.match(UUID_RE)?.[0] ?? input) || null;
|
|
23442
|
+
return {
|
|
23443
|
+
url: null,
|
|
23444
|
+
origin: null,
|
|
23445
|
+
orgSlug: null,
|
|
23446
|
+
projectRef: null,
|
|
23447
|
+
sessionId: null,
|
|
23448
|
+
agentId: null,
|
|
23449
|
+
requestId: input.startsWith("req_") ? input : null,
|
|
23450
|
+
rawId: id
|
|
23451
|
+
};
|
|
23452
|
+
}
|
|
23453
|
+
const parts = url.pathname.split("/").filter(Boolean);
|
|
23454
|
+
const projectsIndex = parts.indexOf("projects");
|
|
23455
|
+
const sessionsIndex = parts.indexOf("sessions");
|
|
23456
|
+
const orgSlug = projectsIndex > 0 ? parts[0] ?? null : null;
|
|
23457
|
+
const projectRef = projectsIndex >= 0 ? parts[projectsIndex + 1] ?? null : null;
|
|
23458
|
+
const sessionId = url.searchParams.get("session") ?? (sessionsIndex >= 0 ? parts[sessionsIndex + 1] ?? null : null);
|
|
23459
|
+
return {
|
|
23460
|
+
url: redactUrl(url),
|
|
23461
|
+
origin: url.origin,
|
|
23462
|
+
orgSlug,
|
|
23463
|
+
projectRef,
|
|
23464
|
+
sessionId,
|
|
23465
|
+
agentId: url.searchParams.get("agent"),
|
|
23466
|
+
requestId: url.searchParams.get("requestId") ?? url.searchParams.get("request_id"),
|
|
23467
|
+
rawId: input.match(UUID_RE)?.[0] ?? null
|
|
23468
|
+
};
|
|
23469
|
+
}
|
|
23470
|
+
function boundedTtl(value) {
|
|
23471
|
+
if (!Number.isFinite(value))
|
|
23472
|
+
return 15;
|
|
23473
|
+
return Math.min(Math.max(Math.round(value ?? 15), 1), 60);
|
|
23474
|
+
}
|
|
23475
|
+
function makeCommand(command) {
|
|
23476
|
+
return command.replace(/\s+/g, " ").trim();
|
|
23477
|
+
}
|
|
23478
|
+
function hostnameFromOrigin(origin) {
|
|
23479
|
+
if (!origin)
|
|
23480
|
+
return null;
|
|
23481
|
+
return safeUrl(origin)?.hostname ?? null;
|
|
23482
|
+
}
|
|
23483
|
+
function originMatches(pattern, origin) {
|
|
23484
|
+
if (!origin)
|
|
23485
|
+
return false;
|
|
23486
|
+
const normalizedPattern = normalizeOrigin(pattern);
|
|
23487
|
+
const normalizedOrigin = normalizeOrigin(origin);
|
|
23488
|
+
if (!normalizedOrigin)
|
|
23489
|
+
return false;
|
|
23490
|
+
if (normalizedPattern === normalizedOrigin)
|
|
23491
|
+
return true;
|
|
23492
|
+
const targetHost = hostnameFromOrigin(normalizedOrigin);
|
|
23493
|
+
const patternHost = normalizedPattern ? hostnameFromOrigin(normalizedPattern) : pattern.replace(/^https?:\/\//, "");
|
|
23494
|
+
if (!targetHost || !patternHost)
|
|
23495
|
+
return false;
|
|
23496
|
+
if (patternHost.startsWith("*.")) {
|
|
23497
|
+
const suffix = patternHost.slice(1);
|
|
23498
|
+
return targetHost.endsWith(suffix);
|
|
23499
|
+
}
|
|
23500
|
+
return targetHost === patternHost;
|
|
23501
|
+
}
|
|
23502
|
+
function resolveProfile(input, target, config) {
|
|
23503
|
+
const apps = config?.apps ?? {};
|
|
23504
|
+
const explicitKey = input.profile?.trim() || input.app?.trim() || config?.defaultProfile;
|
|
23505
|
+
if (explicitKey && apps[explicitKey]) {
|
|
23506
|
+
return {
|
|
23507
|
+
key: explicitKey,
|
|
23508
|
+
profile: apps[explicitKey],
|
|
23509
|
+
matchedOrigin: target.origin
|
|
23510
|
+
};
|
|
23511
|
+
}
|
|
23512
|
+
for (const [key, profile] of Object.entries(apps)) {
|
|
23513
|
+
const match = profile.origins?.find((origin) => originMatches(origin, target.origin));
|
|
23514
|
+
if (match) {
|
|
23515
|
+
return { key, profile, matchedOrigin: match };
|
|
23516
|
+
}
|
|
23517
|
+
}
|
|
23518
|
+
return { key: null, profile: null, matchedOrigin: null };
|
|
23519
|
+
}
|
|
23520
|
+
function firstResolvedCredential(...values) {
|
|
23521
|
+
for (const value of values) {
|
|
23522
|
+
if (!value?.trim())
|
|
23523
|
+
continue;
|
|
23524
|
+
const resolved = resolveCredential(value);
|
|
23525
|
+
if (resolved)
|
|
23526
|
+
return resolved;
|
|
23527
|
+
}
|
|
23528
|
+
return null;
|
|
23529
|
+
}
|
|
23530
|
+
function displayCredential(value, source) {
|
|
23531
|
+
if (!value)
|
|
23532
|
+
return null;
|
|
23533
|
+
if (source && isCredentialReference(source))
|
|
23534
|
+
return "[configured]";
|
|
23535
|
+
return redactProdDebugText(value);
|
|
23536
|
+
}
|
|
23537
|
+
function replacementValues(target, input, supportGrant) {
|
|
23538
|
+
const values = {
|
|
23539
|
+
targetUrl: target.url ?? input.target,
|
|
23540
|
+
origin: target.origin ?? "",
|
|
23541
|
+
org: target.orgSlug ?? "",
|
|
23542
|
+
project: target.projectRef ?? "",
|
|
23543
|
+
session: target.sessionId ?? "",
|
|
23544
|
+
agent: target.agentId ?? "",
|
|
23545
|
+
request: target.requestId ?? "",
|
|
23546
|
+
rawId: target.rawId ?? "",
|
|
23547
|
+
reason: input.reason ?? "",
|
|
23548
|
+
supportGrant: supportGrant ?? ""
|
|
23549
|
+
};
|
|
23550
|
+
for (const [key, value] of Object.entries({ ...values })) {
|
|
23551
|
+
values[`${key}Encoded`] = encodeURIComponent(value);
|
|
23552
|
+
}
|
|
23553
|
+
return values;
|
|
23554
|
+
}
|
|
23555
|
+
function renderTemplate(template, values) {
|
|
23556
|
+
return template.replace(/\{([a-zA-Z0-9_]+)\}/g, (_match, key) => values[key] ?? "");
|
|
23557
|
+
}
|
|
23558
|
+
function resolveSupportGrant(input, profile) {
|
|
23559
|
+
if (input.supportGrantId?.trim()) {
|
|
23560
|
+
return {
|
|
23561
|
+
value: input.supportGrantId.trim(),
|
|
23562
|
+
display: displayCredential(input.supportGrantId.trim()),
|
|
23563
|
+
source: "input"
|
|
23564
|
+
};
|
|
23565
|
+
}
|
|
23566
|
+
const source = profile?.supportGrantRef ?? profile?.supportGrantId ?? null;
|
|
23567
|
+
const value = firstResolvedCredential(profile?.supportGrantRef, profile?.supportGrantId);
|
|
23568
|
+
return { value, display: displayCredential(value, source ?? undefined), source };
|
|
23569
|
+
}
|
|
23570
|
+
function resolveSupportUrl(input, target, profile, supportGrant) {
|
|
23571
|
+
if (input.supportUrl?.trim())
|
|
23572
|
+
return input.supportUrl.trim();
|
|
23573
|
+
const direct = firstResolvedCredential(profile?.supportUrlRef, profile?.supportUrl);
|
|
23574
|
+
if (direct)
|
|
23575
|
+
return direct;
|
|
23576
|
+
if (profile?.supportUrlTemplate) {
|
|
23577
|
+
const rendered = renderTemplate(profile.supportUrlTemplate, replacementValues(target, input, supportGrant)).trim();
|
|
23578
|
+
return rendered || null;
|
|
23579
|
+
}
|
|
23580
|
+
return null;
|
|
23581
|
+
}
|
|
23582
|
+
function resolvePiiOrigin(profile, target) {
|
|
23583
|
+
if (!profile?.piiOrigin)
|
|
23584
|
+
return target.origin;
|
|
23585
|
+
return redactUrlString(renderTemplate(profile.piiOrigin, replacementValues(target, { target: target.url ?? "" }, null)));
|
|
23586
|
+
}
|
|
23587
|
+
function resolveSupportRunTarget(supportUrl, input, target) {
|
|
23588
|
+
if (supportUrl)
|
|
23589
|
+
return redactUrlString(supportUrl);
|
|
23590
|
+
return target.url ?? target.origin ?? redactProdDebugText(input.target);
|
|
23591
|
+
}
|
|
23592
|
+
function supportScenarioDescription(reason) {
|
|
23593
|
+
return `Prod debug: ${reason}. Reproduce the user-visible issue, capture console and network errors, and do not enter secrets.`;
|
|
23594
|
+
}
|
|
23595
|
+
function configuredMissing(profile, supportUrl, supportGrant, includeLogs) {
|
|
23596
|
+
const missing = [];
|
|
23597
|
+
if (!profile) {
|
|
23598
|
+
missing.push("optional: add prodDebug.apps.<profile>.origins to match this app automatically");
|
|
23599
|
+
}
|
|
23600
|
+
if (!supportUrl) {
|
|
23601
|
+
missing.push("supportUrl/supportUrlRef/supportUrlTemplate for scoped browser debugging");
|
|
23602
|
+
}
|
|
23603
|
+
if (!supportGrant) {
|
|
23604
|
+
missing.push("supportGrantId/supportGrantRef for auditable support access");
|
|
23605
|
+
}
|
|
23606
|
+
if (includeLogs && !profile?.logCommand) {
|
|
23607
|
+
missing.push("logCommand for sanitized app/provider log lookup");
|
|
23608
|
+
}
|
|
23609
|
+
return missing;
|
|
23610
|
+
}
|
|
23611
|
+
function createProdDebugPlan(input, config) {
|
|
23612
|
+
const target = parseProdDebugTarget(input.target);
|
|
23613
|
+
const browserRequested = input.includeBrowser !== false;
|
|
23614
|
+
const resolvedProfile = resolveProfile(input, target, config);
|
|
23615
|
+
const supportGrant = resolveSupportGrant(input, resolvedProfile.profile);
|
|
23616
|
+
const supportUrl = resolveSupportUrl(input, target, resolvedProfile.profile, supportGrant.value);
|
|
23617
|
+
const supportBrowserReady = Boolean(supportUrl);
|
|
23618
|
+
const app = input.app?.trim() || resolvedProfile.profile?.name || resolvedProfile.key || (target.origin ? new URL(target.origin).hostname : "app");
|
|
23619
|
+
const reason = input.reason?.trim() || "production debug requested";
|
|
23620
|
+
const actor = input.actor?.trim() || process.env["USER"] || "agent";
|
|
23621
|
+
const ttlMinutes = boundedTtl(input.ttlMinutes);
|
|
23622
|
+
const piiOrigin = resolvePiiOrigin(resolvedProfile.profile, target);
|
|
23623
|
+
const logCommand = resolvedProfile.profile?.logCommand ? redactUrlString(renderTemplate(resolvedProfile.profile.logCommand, replacementValues(target, { ...input, reason }, supportGrant.value))) : null;
|
|
23624
|
+
const safety = [
|
|
23625
|
+
"read-only by default",
|
|
23626
|
+
"no customer passwords or raw cookies",
|
|
23627
|
+
"redact tokens, OAuth codes, session values, support grants, and secrets",
|
|
23628
|
+
"verify org/user/session scope before reading data",
|
|
23629
|
+
"require explicit approval for production writes",
|
|
23630
|
+
`support access TTL capped at ${ttlMinutes} minutes`
|
|
23631
|
+
];
|
|
23632
|
+
const checks = [];
|
|
23633
|
+
const blocked = [];
|
|
23634
|
+
if (target.url) {
|
|
23635
|
+
checks.push({
|
|
23636
|
+
id: "public-route-smoke",
|
|
23637
|
+
status: "ready",
|
|
23638
|
+
description: "Open the supplied production URL and capture console/network errors without credentials.",
|
|
23639
|
+
command: makeCommand(`testers scan all ${JSON.stringify(target.url)} --json`)
|
|
23640
|
+
});
|
|
23641
|
+
}
|
|
23642
|
+
checks.push({
|
|
23643
|
+
id: "pii-redaction-scan",
|
|
23644
|
+
status: piiOrigin ? "ready" : "blocked",
|
|
23645
|
+
description: "Scan public/API responses for accidental sensitive data leakage.",
|
|
23646
|
+
command: piiOrigin ? makeCommand(`testers scan pii ${JSON.stringify(piiOrigin)} --json`) : undefined,
|
|
23647
|
+
reason: piiOrigin ? undefined : "Need a URL origin or prodDebug app profile piiOrigin to run the PII scan."
|
|
23648
|
+
});
|
|
23649
|
+
if (browserRequested) {
|
|
23650
|
+
if (supportBrowserReady) {
|
|
23651
|
+
checks.push({
|
|
23652
|
+
id: "support-browser-repro",
|
|
23653
|
+
status: "ready",
|
|
23654
|
+
description: "Use an audited support browser/session URL to reproduce the user-visible issue.",
|
|
23655
|
+
command: makeCommand(`testers run ${JSON.stringify(resolveSupportRunTarget(supportUrl, input, target))} ${JSON.stringify(supportScenarioDescription(reason))} --headed --json --overall-timeout 600000`)
|
|
23656
|
+
});
|
|
23657
|
+
} else {
|
|
23658
|
+
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.";
|
|
23659
|
+
blocked.push(reasonText);
|
|
23660
|
+
checks.push({
|
|
23661
|
+
id: "support-browser-repro",
|
|
23662
|
+
status: "blocked",
|
|
23663
|
+
description: "Browser reproduction as the target user requires a short-lived audited support session.",
|
|
23664
|
+
reason: reasonText
|
|
23665
|
+
});
|
|
23666
|
+
}
|
|
23667
|
+
}
|
|
23668
|
+
if (input.includeLogs) {
|
|
23669
|
+
if (logCommand) {
|
|
23670
|
+
checks.push({
|
|
23671
|
+
id: "log-timeline",
|
|
23672
|
+
status: "ready",
|
|
23673
|
+
description: "Read sanitized app/provider logs by request ID, session ID, project ID, or support access ID.",
|
|
23674
|
+
command: makeCommand(logCommand)
|
|
23675
|
+
});
|
|
23676
|
+
} else {
|
|
23677
|
+
checks.push({
|
|
23678
|
+
id: "log-timeline",
|
|
23679
|
+
status: "blocked",
|
|
23680
|
+
description: "Read sanitized app/provider logs by request ID, session ID, project ID, or support access ID.",
|
|
23681
|
+
reason: "Configure prodDebug.apps.<profile>.logCommand or use an app-specific log MCP. Do not paste raw provider logs with headers/secrets."
|
|
23682
|
+
});
|
|
23683
|
+
}
|
|
23684
|
+
}
|
|
23685
|
+
if (input.allowWrites) {
|
|
23686
|
+
blocked.push("Production writes are not part of prod-debug. Require a separate explicit approval and app-specific write tool.");
|
|
23687
|
+
}
|
|
23688
|
+
return {
|
|
23689
|
+
target,
|
|
23690
|
+
app,
|
|
23691
|
+
actor,
|
|
23692
|
+
reason,
|
|
23693
|
+
ttlMinutes,
|
|
23694
|
+
setup: {
|
|
23695
|
+
profile: resolvedProfile.key,
|
|
23696
|
+
matchedOrigin: resolvedProfile.matchedOrigin,
|
|
23697
|
+
configured: {
|
|
23698
|
+
supportUrl: Boolean(supportUrl),
|
|
23699
|
+
supportGrant: Boolean(supportGrant.value),
|
|
23700
|
+
piiOrigin: Boolean(piiOrigin),
|
|
23701
|
+
logCommand: Boolean(logCommand)
|
|
23702
|
+
},
|
|
23703
|
+
missing: configuredMissing(resolvedProfile.profile, supportUrl, supportGrant.value, Boolean(input.includeLogs))
|
|
23704
|
+
},
|
|
23705
|
+
supportAccess: {
|
|
23706
|
+
required: browserRequested,
|
|
23707
|
+
grantId: supportGrant.display,
|
|
23708
|
+
browserReady: supportBrowserReady,
|
|
23709
|
+
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."
|
|
23710
|
+
},
|
|
23711
|
+
safety,
|
|
23712
|
+
checks,
|
|
23713
|
+
blocked
|
|
23714
|
+
};
|
|
23715
|
+
}
|
|
23716
|
+
|
|
23717
|
+
// src/mcp/index.ts
|
|
23299
23718
|
var cliArgs = new Set(process.argv.slice(2));
|
|
23300
23719
|
if (cliArgs.has("--help") || cliArgs.has("-h")) {
|
|
23301
23720
|
console.log(`Usage: testers-mcp [options]
|
|
@@ -23309,7 +23728,7 @@ Options:
|
|
|
23309
23728
|
process.exit(0);
|
|
23310
23729
|
}
|
|
23311
23730
|
if (cliArgs.has("--version") || cliArgs.has("-V")) {
|
|
23312
|
-
console.log(
|
|
23731
|
+
console.log(package_default.version);
|
|
23313
23732
|
process.exit(0);
|
|
23314
23733
|
}
|
|
23315
23734
|
function json(data) {
|
|
@@ -23370,7 +23789,27 @@ var ID_DESC = "Accepts either the full UUID (e.g. 'abc123...') or the short ID (
|
|
|
23370
23789
|
var MODEL_DESC = "Model to use. Values: 'quick' (claude-haiku-4-5, cheapest), 'thorough' (claude-sonnet-4-6, balanced), 'deep' (claude-opus-4-6, most capable). Default: 'quick'.";
|
|
23371
23790
|
var server = new McpServer({
|
|
23372
23791
|
name: "testers",
|
|
23373
|
-
version:
|
|
23792
|
+
version: package_default.version
|
|
23793
|
+
});
|
|
23794
|
+
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.", {
|
|
23795
|
+
target: exports_external.string().describe("Production URL, session ID, project ID, request ID, or other target evidence"),
|
|
23796
|
+
app: exports_external.string().optional().describe("App name for reporting"),
|
|
23797
|
+
profile: exports_external.string().optional().describe("prodDebug app profile from testers config"),
|
|
23798
|
+
actor: exports_external.string().optional().describe("Operator/agent identity for audit context"),
|
|
23799
|
+
reason: exports_external.string().optional().describe("Debug reason or support context"),
|
|
23800
|
+
supportUrl: exports_external.string().optional().describe("Audited support browser/session URL minted by the target app"),
|
|
23801
|
+
supportGrantId: exports_external.string().optional().describe("Audited support access grant ID"),
|
|
23802
|
+
ttlMinutes: exports_external.number().optional().describe("Support access TTL in minutes, capped at 60"),
|
|
23803
|
+
includeBrowser: exports_external.boolean().optional().describe("Include user-scoped browser reproduction check"),
|
|
23804
|
+
includeLogs: exports_external.boolean().optional().describe("Include log timeline adapter requirement"),
|
|
23805
|
+
allowWrites: exports_external.boolean().optional().describe("Document that writes require a separate explicit approval")
|
|
23806
|
+
}, async (input) => {
|
|
23807
|
+
try {
|
|
23808
|
+
const config = loadConfig();
|
|
23809
|
+
return json(createProdDebugPlan(input, config.prodDebug));
|
|
23810
|
+
} catch (error) {
|
|
23811
|
+
return errorResponse(error);
|
|
23812
|
+
}
|
|
23374
23813
|
});
|
|
23375
23814
|
server.tool("create_scenario", "Create a new test scenario", {
|
|
23376
23815
|
name: exports_external.string().describe("Scenario name"),
|
|
@@ -23390,15 +23829,16 @@ server.tool("create_scenario", "Create a new test scenario", {
|
|
|
23390
23829
|
return errorResponse(error);
|
|
23391
23830
|
}
|
|
23392
23831
|
});
|
|
23393
|
-
server.tool("batch_create_scenarios", "Create multiple test scenarios in a single call. Each item requires name
|
|
23832
|
+
server.tool("batch_create_scenarios", "Create multiple test scenarios in a single call. Each item requires name; description defaults to the name.", {
|
|
23394
23833
|
scenarios: exports_external.array(exports_external.object({
|
|
23395
23834
|
name: exports_external.string().describe("Scenario name"),
|
|
23396
|
-
description: exports_external.string().describe("What this scenario tests"),
|
|
23835
|
+
description: exports_external.string().optional().describe("What this scenario tests"),
|
|
23397
23836
|
steps: exports_external.array(exports_external.string()).optional().describe("Ordered test steps"),
|
|
23398
23837
|
tags: exports_external.array(exports_external.string()).optional().describe("Tags for filtering"),
|
|
23399
23838
|
priority: exports_external.enum(["low", "medium", "high", "critical"]).optional().describe("Scenario priority"),
|
|
23400
23839
|
model: exports_external.string().optional().describe(MODEL_DESC),
|
|
23401
23840
|
targetPath: exports_external.string().optional().describe("URL path to navigate to"),
|
|
23841
|
+
url: exports_external.string().optional().describe("Alias for targetPath"),
|
|
23402
23842
|
requiresAuth: exports_external.boolean().optional().describe("Whether scenario requires authentication")
|
|
23403
23843
|
})).min(1).max(100).describe("Array of scenarios to create"),
|
|
23404
23844
|
projectId: exports_external.string().optional().describe("Project ID to scope all scenarios to")
|
|
@@ -23407,7 +23847,12 @@ server.tool("batch_create_scenarios", "Create multiple test scenarios in a singl
|
|
|
23407
23847
|
const results = [];
|
|
23408
23848
|
for (const s of scenarios) {
|
|
23409
23849
|
try {
|
|
23410
|
-
const scenario = createScenario({
|
|
23850
|
+
const scenario = createScenario({
|
|
23851
|
+
...s,
|
|
23852
|
+
description: s.description ?? s.name,
|
|
23853
|
+
targetPath: s.targetPath ?? s.url,
|
|
23854
|
+
projectId
|
|
23855
|
+
});
|
|
23411
23856
|
results.push({ id: scenario.id, name: scenario.name, shortId: scenario.shortId });
|
|
23412
23857
|
} catch (e) {
|
|
23413
23858
|
results.push({ id: "", name: s.name, shortId: "", error: e instanceof Error ? e.message : String(e) });
|
|
@@ -23791,41 +24236,6 @@ server.tool("get_run_costs", "Get cost breakdown for a run, with per-scenario de
|
|
|
23791
24236
|
return errorResponse(error);
|
|
23792
24237
|
}
|
|
23793
24238
|
});
|
|
23794
|
-
server.tool("batch_create_scenarios", "Create multiple scenarios in a single call. Returns created scenarios and any failures.", {
|
|
23795
|
-
scenarios: exports_external.array(exports_external.object({
|
|
23796
|
-
name: exports_external.string().describe("Scenario name"),
|
|
23797
|
-
url: exports_external.string().optional().describe("Target URL (stored as targetPath)"),
|
|
23798
|
-
description: exports_external.string().optional().describe("What this scenario tests"),
|
|
23799
|
-
steps: exports_external.array(exports_external.string()).optional().describe("Ordered test steps"),
|
|
23800
|
-
tags: exports_external.array(exports_external.string()).optional().describe("Tags for filtering"),
|
|
23801
|
-
priority: exports_external.enum(["low", "medium", "high", "critical"]).optional().describe("Scenario priority")
|
|
23802
|
-
})).describe("Array of scenarios to create")
|
|
23803
|
-
}, async ({ scenarios }) => {
|
|
23804
|
-
const created = [];
|
|
23805
|
-
const failed = [];
|
|
23806
|
-
for (let i = 0;i < scenarios.length; i++) {
|
|
23807
|
-
const input = scenarios[i];
|
|
23808
|
-
try {
|
|
23809
|
-
const scenario = createScenario({
|
|
23810
|
-
name: input.name,
|
|
23811
|
-
description: input.description ?? input.name,
|
|
23812
|
-
steps: input.steps,
|
|
23813
|
-
tags: input.tags,
|
|
23814
|
-
priority: input.priority,
|
|
23815
|
-
targetPath: input.url
|
|
23816
|
-
});
|
|
23817
|
-
created.push(scenario);
|
|
23818
|
-
} catch (error) {
|
|
23819
|
-
const e = error instanceof Error ? error : new Error(String(error));
|
|
23820
|
-
failed.push({ index: i, name: input.name, error: e.message });
|
|
23821
|
-
}
|
|
23822
|
-
}
|
|
23823
|
-
const lines = [
|
|
23824
|
-
`Created: ${created.length} scenario(s)`,
|
|
23825
|
-
...created.map((s) => ` [${s.shortId}] ${s.name}`)
|
|
23826
|
-
];
|
|
23827
|
-
return json({ created, failed });
|
|
23828
|
-
});
|
|
23829
24239
|
server.tool("cancel_run", "Mark a run as cancelled in the database. In-flight browser processes may still complete but results will be ignored.", {
|
|
23830
24240
|
runId: exports_external.string().describe("Run ID to cancel")
|
|
23831
24241
|
}, async ({ runId }) => {
|
package/dist/sdk/index.d.ts
CHANGED
|
@@ -7,8 +7,7 @@
|
|
|
7
7
|
* import { createScenario, listScenarioTemplates, RunOptions } from "open-testers/sdk";
|
|
8
8
|
* ```
|
|
9
9
|
*/
|
|
10
|
-
export type { Scenario, Run, Result,
|
|
11
|
-
export type { RunEvent } from "../lib/runner.js";
|
|
10
|
+
export type { Scenario, Run, Result, CreateScenarioInput, UpdateScenarioInput, Persona, CreatePersonaInput, UpdatePersonaInput, PersonaAuth, AuthProfile, AuthStrategy, Assertion, AssertionType, ScenarioPriority, ResultStatus, ModelPreset, BrowserEngine, } from "../types/index.js";
|
|
12
11
|
export type { MockRule } from "../lib/network-mock.js";
|
|
13
12
|
export type { BatchAction, BatchActionResult } from "../lib/batch-actions.js";
|
|
14
13
|
export type { MutationEvent, MutationOptions } from "../lib/dom-mutation.js";
|
|
@@ -17,6 +16,7 @@ export type { A11yAuditResult, A11yAuditOptions, A11yViolation } from "../lib/a1
|
|
|
17
16
|
export type { Environment, EnvironmentInfo } from "../lib/environment.js";
|
|
18
17
|
export type { ThrottleProfile } from "../lib/offline-mode.js";
|
|
19
18
|
export type { ChainOutput, ChainLink } from "../lib/scenario-chain.js";
|
|
19
|
+
export type { Webhook, WebhookPayload, ApiCheckWebhookPayload } from "../lib/webhooks.js";
|
|
20
20
|
export { createScenario, getScenario, getScenarioByShortId, listScenarios, updateScenario, deleteScenario, findStaleScenarios, } from "../db/scenarios.js";
|
|
21
21
|
export { getRun, listRuns, updateRun, countRuns, } from "../db/runs.js";
|
|
22
22
|
export { createResult, getResult, listResults, getResultsByRun, updateResult, } from "../db/results.js";
|
|
@@ -27,6 +27,7 @@ export { generateHtmlReport, generateLatestReport, imageToBase64, } from "../lib
|
|
|
27
27
|
export { saveHtmlReport, generatePdfReport, } from "../lib/pdf-export.js";
|
|
28
28
|
export { toJUnitXml, } from "../lib/junit-export.js";
|
|
29
29
|
export { DEVICE_PRESETS, setDevicePreset, setViewport, captureResponsiveScreenshots, isMobileViewport, listDevicePresets, } from "../lib/responsive.js";
|
|
30
|
+
export type { DevicePreset } from "../lib/responsive.js";
|
|
30
31
|
export { batchActions, hasBatchFailures, formatBatchResults, } from "../lib/batch-actions.js";
|
|
31
32
|
export { watchMutations, waitForElement, waitForElementRemoved, waitForText, snapshotDOM, compareSnapshots, extractElements, } from "../lib/dom-mutation.js";
|
|
32
33
|
export { collectPerformanceMetrics, collectWebVitals, checkBudget, formatPerformanceResult, DEFAULT_BUDGET, } from "../lib/performance.js";
|
package/dist/sdk/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/sdk/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAIH,YAAY,EACV,QAAQ,EACR,GAAG,EACH,MAAM,EACN,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/sdk/index.ts"],"names":[],"mappings":"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"}
|