@hasna/testers 0.0.33 → 0.0.35
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/dist/cli/index.js +880 -351
- package/dist/db/workflows.d.ts.map +1 -1
- package/dist/index.d.ts +4 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +944 -190
- package/dist/lib/ai-client.d.ts +2 -0
- package/dist/lib/ai-client.d.ts.map +1 -1
- package/dist/lib/assertions.d.ts +4 -1
- package/dist/lib/assertions.d.ts.map +1 -1
- package/dist/lib/repo-discovery.d.ts.map +1 -1
- package/dist/lib/repo-executor.d.ts.map +1 -1
- package/dist/lib/runner.d.ts +29 -1
- package/dist/lib/runner.d.ts.map +1 -1
- package/dist/lib/workflow-runner.d.ts +73 -5
- package/dist/lib/workflow-runner.d.ts.map +1 -1
- package/dist/mcp/http.d.ts +1 -0
- package/dist/mcp/http.d.ts.map +1 -1
- package/dist/mcp/index.js +668 -130
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/sdk/index.d.ts +3 -3
- package/dist/sdk/index.d.ts.map +1 -1
- package/dist/server/index.js +634 -108
- package/dist/types/index.d.ts +23 -3
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +7 -6
package/dist/mcp/index.js
CHANGED
|
@@ -52,7 +52,7 @@ var package_default;
|
|
|
52
52
|
var init_package = __esm(() => {
|
|
53
53
|
package_default = {
|
|
54
54
|
name: "@hasna/testers",
|
|
55
|
-
version: "0.0.
|
|
55
|
+
version: "0.0.35",
|
|
56
56
|
description: "AI-powered QA testing CLI \u2014 spawns cheap AI agents to test web apps with headless browsers",
|
|
57
57
|
type: "module",
|
|
58
58
|
main: "dist/index.js",
|
|
@@ -76,10 +76,10 @@ var init_package = __esm(() => {
|
|
|
76
76
|
],
|
|
77
77
|
scripts: {
|
|
78
78
|
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",
|
|
79
|
-
"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",
|
|
80
|
-
"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",
|
|
81
|
-
"build:server": "bun build src/server/index.ts --outdir dist/server --target bun --external @anthropic-ai/sdk --external playwright --external @hasna/browser",
|
|
82
|
-
"build:lib": "bun build src/index.ts --outdir dist --target bun --external playwright --external @anthropic-ai/sdk --external @modelcontextprotocol/sdk --external @hasna/browser",
|
|
79
|
+
"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 --external @hasna/sandboxes",
|
|
80
|
+
"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 --external @hasna/sandboxes",
|
|
81
|
+
"build:server": "bun build src/server/index.ts --outdir dist/server --target bun --external @anthropic-ai/sdk --external playwright --external @hasna/browser --external @hasna/sandboxes",
|
|
82
|
+
"build:lib": "bun build src/index.ts --outdir dist --target bun --external playwright --external @anthropic-ai/sdk --external @modelcontextprotocol/sdk --external @hasna/browser --external @hasna/sandboxes",
|
|
83
83
|
"build:types": "NODE_OPTIONS='--max-old-space-size=8192' tsc --emitDeclarationOnly --outDir dist --skipLibCheck || true",
|
|
84
84
|
"build:dashboard": "cd dashboard && bun run build",
|
|
85
85
|
"build:ext": "cd extension && bun run build",
|
|
@@ -93,10 +93,11 @@ var init_package = __esm(() => {
|
|
|
93
93
|
},
|
|
94
94
|
dependencies: {
|
|
95
95
|
"@anthropic-ai/sdk": "^0.52.0",
|
|
96
|
-
"@hasna/browser": "^0.4.
|
|
96
|
+
"@hasna/browser": "^0.4.12",
|
|
97
97
|
"@hasna/cloud": "^0.1.24",
|
|
98
98
|
"@hasna/contacts": "^0.6.8",
|
|
99
99
|
"@hasna/projects": "^0.1.42",
|
|
100
|
+
"@hasna/sandboxes": "^0.1.27",
|
|
100
101
|
"@modelcontextprotocol/sdk": "^1.12.1",
|
|
101
102
|
ai: "^6.0.175",
|
|
102
103
|
chalk: "^5.4.1",
|
|
@@ -14134,6 +14135,56 @@ See https://www.postgresql.org/docs/current/libpq-ssl.html for libpq SSL mode de
|
|
|
14134
14135
|
});
|
|
14135
14136
|
|
|
14136
14137
|
// src/types/index.ts
|
|
14138
|
+
function isRecord(value) {
|
|
14139
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
14140
|
+
}
|
|
14141
|
+
function stringValue(value) {
|
|
14142
|
+
return typeof value === "string" && value.trim() ? value : undefined;
|
|
14143
|
+
}
|
|
14144
|
+
function numberValue(value) {
|
|
14145
|
+
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
14146
|
+
}
|
|
14147
|
+
function stringMap(value) {
|
|
14148
|
+
if (!isRecord(value))
|
|
14149
|
+
return;
|
|
14150
|
+
const entries = Object.entries(value).filter((entry) => typeof entry[1] === "string");
|
|
14151
|
+
return entries.length > 0 ? Object.fromEntries(entries) : undefined;
|
|
14152
|
+
}
|
|
14153
|
+
function cleanupValue(value) {
|
|
14154
|
+
if (value === "delete" || value === "stop" || value === "keep")
|
|
14155
|
+
return value;
|
|
14156
|
+
return;
|
|
14157
|
+
}
|
|
14158
|
+
function workflowExecutionFromValue(value) {
|
|
14159
|
+
const input = isRecord(value) ? value : {};
|
|
14160
|
+
const rawTarget = stringValue(input["target"]) ?? "local";
|
|
14161
|
+
if (rawTarget === "local") {
|
|
14162
|
+
const timeoutMs2 = numberValue(input["timeoutMs"]);
|
|
14163
|
+
return timeoutMs2 === undefined ? { target: "local" } : { target: "local", timeoutMs: timeoutMs2 };
|
|
14164
|
+
}
|
|
14165
|
+
if (rawTarget !== "sandbox" && rawTarget !== "connector:e2b") {
|
|
14166
|
+
throw new Error(`Unsupported workflow execution target: ${rawTarget}`);
|
|
14167
|
+
}
|
|
14168
|
+
const provider = rawTarget === "connector:e2b" ? "e2b" : stringValue(input["provider"]) ?? stringValue(input["connector"]);
|
|
14169
|
+
const sandboxImage = stringValue(input["sandboxImage"]) ?? stringValue(input["sandboxTemplate"]);
|
|
14170
|
+
const sandboxRemoteDir = stringValue(input["sandboxRemoteDir"]);
|
|
14171
|
+
const sandboxCleanup = cleanupValue(input["sandboxCleanup"]);
|
|
14172
|
+
const setupCommand = stringValue(input["setupCommand"]);
|
|
14173
|
+
const packageSpec = stringValue(input["packageSpec"]);
|
|
14174
|
+
const timeoutMs = numberValue(input["timeoutMs"]);
|
|
14175
|
+
const env = stringMap(input["env"]);
|
|
14176
|
+
return {
|
|
14177
|
+
target: "sandbox",
|
|
14178
|
+
...provider ? { provider } : {},
|
|
14179
|
+
...sandboxImage ? { sandboxImage } : {},
|
|
14180
|
+
...sandboxRemoteDir ? { sandboxRemoteDir } : {},
|
|
14181
|
+
...sandboxCleanup ? { sandboxCleanup } : {},
|
|
14182
|
+
...setupCommand ? { setupCommand } : {},
|
|
14183
|
+
...packageSpec ? { packageSpec } : {},
|
|
14184
|
+
...timeoutMs !== undefined ? { timeoutMs } : {},
|
|
14185
|
+
...env ? { env } : {}
|
|
14186
|
+
};
|
|
14187
|
+
}
|
|
14137
14188
|
function workflowFromRow(row) {
|
|
14138
14189
|
return {
|
|
14139
14190
|
id: row.id,
|
|
@@ -14143,7 +14194,7 @@ function workflowFromRow(row) {
|
|
|
14143
14194
|
scenarioFilter: JSON.parse(row.scenario_filter || "{}"),
|
|
14144
14195
|
personaIds: JSON.parse(row.persona_ids || "[]"),
|
|
14145
14196
|
goal: row.goal ? JSON.parse(row.goal) : null,
|
|
14146
|
-
execution: JSON.parse(row.execution || '{"target":"local"}'),
|
|
14197
|
+
execution: workflowExecutionFromValue(JSON.parse(row.execution || '{"target":"local"}')),
|
|
14147
14198
|
settings: JSON.parse(row.settings || "{}"),
|
|
14148
14199
|
enabled: row.enabled === 1,
|
|
14149
14200
|
createdAt: row.created_at,
|
|
@@ -16696,6 +16747,7 @@ __export(exports_ai_client, {
|
|
|
16696
16747
|
createClientForModel: () => createClientForModel,
|
|
16697
16748
|
createClient: () => createClient,
|
|
16698
16749
|
callOpenAICompatible: () => callOpenAICompatible,
|
|
16750
|
+
buildScenarioUserMessage: () => buildScenarioUserMessage,
|
|
16699
16751
|
BROWSER_TOOLS: () => BROWSER_TOOLS
|
|
16700
16752
|
});
|
|
16701
16753
|
import Anthropic2 from "@anthropic-ai/sdk";
|
|
@@ -17108,7 +17160,6 @@ async function executeTool(page, screenshotter, toolName, toolInput, context) {
|
|
|
17108
17160
|
const assertionType = toolInput.assertion_type;
|
|
17109
17161
|
const selector = toolInput.selector;
|
|
17110
17162
|
const expected = toolInput.expected;
|
|
17111
|
-
const sessionId = context.sessionId ?? "default";
|
|
17112
17163
|
switch (assertionType) {
|
|
17113
17164
|
case "element_exists": {
|
|
17114
17165
|
if (!selector)
|
|
@@ -17173,7 +17224,6 @@ async function executeTool(page, screenshotter, toolName, toolInput, context) {
|
|
|
17173
17224
|
case "browser_intercept": {
|
|
17174
17225
|
const action = toolInput.action;
|
|
17175
17226
|
const pattern = toolInput.pattern;
|
|
17176
|
-
const interceptAction = toolInput.intercept_action;
|
|
17177
17227
|
const statusCode = toolInput.status_code;
|
|
17178
17228
|
const body = toolInput.body;
|
|
17179
17229
|
const sessionId = context.sessionId ?? "default";
|
|
@@ -17250,7 +17300,28 @@ ${JSON.stringify(har, null, 2)}` };
|
|
|
17250
17300
|
}
|
|
17251
17301
|
case "browser_a11y": {
|
|
17252
17302
|
const level = toolInput.level ?? "AA";
|
|
17253
|
-
const snapshot = await page.
|
|
17303
|
+
const snapshot = await page.evaluate(() => {
|
|
17304
|
+
function readRole(el) {
|
|
17305
|
+
return el.getAttribute("role") ?? el.tagName.toLowerCase();
|
|
17306
|
+
}
|
|
17307
|
+
function readName(el) {
|
|
17308
|
+
const labelledBy = el.getAttribute("aria-labelledby");
|
|
17309
|
+
if (labelledBy) {
|
|
17310
|
+
const labelledText = labelledBy.split(/\s+/).map((id) => document.getElementById(id)?.textContent?.trim()).filter(Boolean).join(" ");
|
|
17311
|
+
if (labelledText)
|
|
17312
|
+
return labelledText;
|
|
17313
|
+
}
|
|
17314
|
+
return el.getAttribute("aria-label") ?? el.getAttribute("alt") ?? el.textContent?.trim() ?? "";
|
|
17315
|
+
}
|
|
17316
|
+
function walk(el) {
|
|
17317
|
+
return {
|
|
17318
|
+
role: readRole(el),
|
|
17319
|
+
name: readName(el),
|
|
17320
|
+
children: Array.from(el.children).map((child) => walk(child))
|
|
17321
|
+
};
|
|
17322
|
+
}
|
|
17323
|
+
return document.body ? walk(document.body) : null;
|
|
17324
|
+
});
|
|
17254
17325
|
if (!snapshot)
|
|
17255
17326
|
return { result: "Error: could not capture accessibility tree" };
|
|
17256
17327
|
const issues = [];
|
|
@@ -17292,6 +17363,38 @@ ${filtered.join(`
|
|
|
17292
17363
|
return { result: `Error executing ${toolName}: ${message}` };
|
|
17293
17364
|
}
|
|
17294
17365
|
}
|
|
17366
|
+
function resolveStartUrl(baseUrl, targetPath) {
|
|
17367
|
+
try {
|
|
17368
|
+
return new URL(targetPath, baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`).toString();
|
|
17369
|
+
} catch {
|
|
17370
|
+
return `${baseUrl.replace(/\/+$/, "")}/${targetPath.replace(/^\/+/, "")}`;
|
|
17371
|
+
}
|
|
17372
|
+
}
|
|
17373
|
+
function buildScenarioUserMessage(scenario, baseUrl) {
|
|
17374
|
+
const userParts = [
|
|
17375
|
+
`**Scenario:** ${scenario.name}`,
|
|
17376
|
+
`**Description:** ${scenario.description}`
|
|
17377
|
+
];
|
|
17378
|
+
if (baseUrl) {
|
|
17379
|
+
const normalizedBaseUrl = baseUrl.replace(/\/+$/, "");
|
|
17380
|
+
userParts.push(`**Base URL:** ${normalizedBaseUrl}`);
|
|
17381
|
+
if (scenario.targetPath) {
|
|
17382
|
+
userParts.push(`**Start URL:** ${resolveStartUrl(normalizedBaseUrl, scenario.targetPath)}`);
|
|
17383
|
+
}
|
|
17384
|
+
userParts.push("**Navigation Boundary:** Treat the Base URL as the application under test. Resolve relative paths and in-app navigation against this origin. Do not navigate to another host unless a step explicitly includes an absolute external URL.");
|
|
17385
|
+
}
|
|
17386
|
+
if (scenario.targetPath) {
|
|
17387
|
+
userParts.push(`**Target Path:** ${scenario.targetPath}`);
|
|
17388
|
+
}
|
|
17389
|
+
if (scenario.steps.length > 0) {
|
|
17390
|
+
userParts.push("**Steps:**");
|
|
17391
|
+
for (let i = 0;i < scenario.steps.length; i++) {
|
|
17392
|
+
userParts.push(`${i + 1}. ${scenario.steps[i]}`);
|
|
17393
|
+
}
|
|
17394
|
+
}
|
|
17395
|
+
return userParts.join(`
|
|
17396
|
+
`);
|
|
17397
|
+
}
|
|
17295
17398
|
async function runAgentLoop(options) {
|
|
17296
17399
|
const {
|
|
17297
17400
|
client,
|
|
@@ -17301,6 +17404,7 @@ async function runAgentLoop(options) {
|
|
|
17301
17404
|
model,
|
|
17302
17405
|
runId,
|
|
17303
17406
|
sessionId,
|
|
17407
|
+
baseUrl,
|
|
17304
17408
|
maxTurns = 30,
|
|
17305
17409
|
onStep,
|
|
17306
17410
|
persona,
|
|
@@ -17348,21 +17452,7 @@ Instructions: ${persona.instructions}` : "",
|
|
|
17348
17452
|
"- Verify both positive and negative states"
|
|
17349
17453
|
].join(`
|
|
17350
17454
|
`) + personaSection;
|
|
17351
|
-
const
|
|
17352
|
-
`**Scenario:** ${scenario.name}`,
|
|
17353
|
-
`**Description:** ${scenario.description}`
|
|
17354
|
-
];
|
|
17355
|
-
if (scenario.targetPath) {
|
|
17356
|
-
userParts.push(`**Target Path:** ${scenario.targetPath}`);
|
|
17357
|
-
}
|
|
17358
|
-
if (scenario.steps.length > 0) {
|
|
17359
|
-
userParts.push("**Steps:**");
|
|
17360
|
-
for (let i = 0;i < scenario.steps.length; i++) {
|
|
17361
|
-
userParts.push(`${i + 1}. ${scenario.steps[i]}`);
|
|
17362
|
-
}
|
|
17363
|
-
}
|
|
17364
|
-
const userMessage = userParts.join(`
|
|
17365
|
-
`);
|
|
17455
|
+
const userMessage = buildScenarioUserMessage(scenario, baseUrl);
|
|
17366
17456
|
const screenshots = [];
|
|
17367
17457
|
let tokensUsed = 0;
|
|
17368
17458
|
let stepNumber = 0;
|
|
@@ -17425,7 +17515,7 @@ Instructions: ${persona.instructions}` : "",
|
|
|
17425
17515
|
if (onStep) {
|
|
17426
17516
|
onStep({ type: "tool_call", toolName: toolBlock.name, toolInput, stepNumber });
|
|
17427
17517
|
}
|
|
17428
|
-
const execResult = await executeTool(page, screenshotter, toolBlock.name, toolInput, { runId, scenarioSlug, stepNumber, sessionId, a11y });
|
|
17518
|
+
const execResult = await executeTool(page, screenshotter, toolBlock.name, toolInput, { runId, scenarioSlug, stepNumber, sessionId: sessionId ?? runId, a11y });
|
|
17429
17519
|
if (onStep) {
|
|
17430
17520
|
onStep({ type: "tool_result", toolName: toolBlock.name, toolResult: execResult.result, stepNumber });
|
|
17431
17521
|
}
|
|
@@ -20624,6 +20714,292 @@ var init_failure_pipeline = __esm(() => {
|
|
|
20624
20714
|
init_todos_connector();
|
|
20625
20715
|
});
|
|
20626
20716
|
|
|
20717
|
+
// src/lib/a11y-audit.ts
|
|
20718
|
+
async function runA11yAudit(page, options = {}) {
|
|
20719
|
+
const { level = "AA", rules, exclude = [] } = options;
|
|
20720
|
+
await page.addScriptTag({ url: "https://cdnjs.cloudflare.com/ajax/libs/axe-core/4.9.1/axe.min.js" });
|
|
20721
|
+
const config = {
|
|
20722
|
+
runOnly: {
|
|
20723
|
+
type: level === "AAA" ? "standard" : "tag",
|
|
20724
|
+
values: level === "AAA" ? undefined : [level, "best-practice"]
|
|
20725
|
+
}
|
|
20726
|
+
};
|
|
20727
|
+
if (rules && rules.length > 0) {
|
|
20728
|
+
config.rules = Object.fromEntries(rules.map((r) => [r, { enabled: true }]));
|
|
20729
|
+
}
|
|
20730
|
+
if (exclude.length > 0) {
|
|
20731
|
+
config.exclude = exclude;
|
|
20732
|
+
}
|
|
20733
|
+
const result = await page.evaluate(async (auditConfig) => {
|
|
20734
|
+
const axeResult = await window.axe.run(auditConfig);
|
|
20735
|
+
return axeResult;
|
|
20736
|
+
}, config);
|
|
20737
|
+
const violations = (result.violations ?? []).map((v) => ({
|
|
20738
|
+
id: v.id,
|
|
20739
|
+
impact: v.impact,
|
|
20740
|
+
description: v.description,
|
|
20741
|
+
help: v.help,
|
|
20742
|
+
helpUrl: v.helpUrl,
|
|
20743
|
+
nodes: (v.nodes ?? []).map((n) => ({
|
|
20744
|
+
html: n.html,
|
|
20745
|
+
target: n.target,
|
|
20746
|
+
failureSummary: n.failureSummary
|
|
20747
|
+
}))
|
|
20748
|
+
}));
|
|
20749
|
+
const passes = (result.passes ?? []).map((p) => ({
|
|
20750
|
+
id: p.id,
|
|
20751
|
+
description: p.description
|
|
20752
|
+
}));
|
|
20753
|
+
const incomplete = (result.incomplete ?? []).map((i) => ({
|
|
20754
|
+
id: i.id,
|
|
20755
|
+
description: i.description,
|
|
20756
|
+
impact: i.impact
|
|
20757
|
+
}));
|
|
20758
|
+
const criticalCount = violations.filter((v) => v.impact === "critical").length;
|
|
20759
|
+
const seriousCount = violations.filter((v) => v.impact === "serious").length;
|
|
20760
|
+
const moderateCount = violations.filter((v) => v.impact === "moderate").length;
|
|
20761
|
+
const minorCount = violations.filter((v) => v.impact === "minor").length;
|
|
20762
|
+
return {
|
|
20763
|
+
violations,
|
|
20764
|
+
passes,
|
|
20765
|
+
incomplete,
|
|
20766
|
+
url: page.url(),
|
|
20767
|
+
timestamp: new Date().toISOString(),
|
|
20768
|
+
totalViolations: violations.length,
|
|
20769
|
+
criticalCount,
|
|
20770
|
+
seriousCount,
|
|
20771
|
+
moderateCount,
|
|
20772
|
+
minorCount
|
|
20773
|
+
};
|
|
20774
|
+
}
|
|
20775
|
+
|
|
20776
|
+
// src/lib/assertions.ts
|
|
20777
|
+
async function evaluateAssertions(page, assertions, context = {}) {
|
|
20778
|
+
const results = [];
|
|
20779
|
+
for (const assertion of assertions) {
|
|
20780
|
+
try {
|
|
20781
|
+
const result = await evaluateOne(page, assertion, context);
|
|
20782
|
+
results.push(result);
|
|
20783
|
+
} catch (err) {
|
|
20784
|
+
results.push({
|
|
20785
|
+
assertion,
|
|
20786
|
+
passed: false,
|
|
20787
|
+
actual: "",
|
|
20788
|
+
error: err instanceof Error ? err.message : String(err)
|
|
20789
|
+
});
|
|
20790
|
+
}
|
|
20791
|
+
}
|
|
20792
|
+
return results;
|
|
20793
|
+
}
|
|
20794
|
+
async function evaluateOne(page, assertion, context) {
|
|
20795
|
+
switch (assertion.type) {
|
|
20796
|
+
case "visible": {
|
|
20797
|
+
const visible = await page.locator(assertion.selector).isVisible();
|
|
20798
|
+
return {
|
|
20799
|
+
assertion,
|
|
20800
|
+
passed: visible,
|
|
20801
|
+
actual: String(visible)
|
|
20802
|
+
};
|
|
20803
|
+
}
|
|
20804
|
+
case "not_visible": {
|
|
20805
|
+
const visible = await page.locator(assertion.selector).isVisible();
|
|
20806
|
+
return {
|
|
20807
|
+
assertion,
|
|
20808
|
+
passed: !visible,
|
|
20809
|
+
actual: String(visible)
|
|
20810
|
+
};
|
|
20811
|
+
}
|
|
20812
|
+
case "text_contains": {
|
|
20813
|
+
const text = await page.locator(assertion.selector).textContent() ?? "";
|
|
20814
|
+
const expected = String(assertion.expected ?? "");
|
|
20815
|
+
return {
|
|
20816
|
+
assertion,
|
|
20817
|
+
passed: text.includes(expected),
|
|
20818
|
+
actual: text
|
|
20819
|
+
};
|
|
20820
|
+
}
|
|
20821
|
+
case "text_equals": {
|
|
20822
|
+
const text = await page.locator(assertion.selector).textContent() ?? "";
|
|
20823
|
+
const expected = String(assertion.expected ?? "");
|
|
20824
|
+
return {
|
|
20825
|
+
assertion,
|
|
20826
|
+
passed: text.trim() === expected.trim(),
|
|
20827
|
+
actual: text
|
|
20828
|
+
};
|
|
20829
|
+
}
|
|
20830
|
+
case "element_count": {
|
|
20831
|
+
const count = await page.locator(assertion.selector).count();
|
|
20832
|
+
const expected = Number(assertion.expected ?? 0);
|
|
20833
|
+
return {
|
|
20834
|
+
assertion,
|
|
20835
|
+
passed: count === expected,
|
|
20836
|
+
actual: String(count)
|
|
20837
|
+
};
|
|
20838
|
+
}
|
|
20839
|
+
case "no_console_errors": {
|
|
20840
|
+
if (context.consoleErrors !== undefined) {
|
|
20841
|
+
const errors2 = context.consoleErrors.filter(Boolean);
|
|
20842
|
+
return {
|
|
20843
|
+
assertion,
|
|
20844
|
+
passed: errors2.length === 0,
|
|
20845
|
+
actual: errors2.length === 0 ? "No console errors captured" : errors2.slice(0, 3).join(" | ")
|
|
20846
|
+
};
|
|
20847
|
+
}
|
|
20848
|
+
const errorElements = await page.locator('[role="alert"], .error, .error-message, [data-testid="error"]').count();
|
|
20849
|
+
return {
|
|
20850
|
+
assertion,
|
|
20851
|
+
passed: errorElements === 0,
|
|
20852
|
+
actual: `${errorElements} error element(s) found`
|
|
20853
|
+
};
|
|
20854
|
+
}
|
|
20855
|
+
case "no_a11y_violations": {
|
|
20856
|
+
try {
|
|
20857
|
+
const auditResult = await runA11yAudit(page);
|
|
20858
|
+
const hasIssues = auditResult.violations.length > 0;
|
|
20859
|
+
return {
|
|
20860
|
+
assertion,
|
|
20861
|
+
passed: !hasIssues,
|
|
20862
|
+
actual: hasIssues ? `${auditResult.totalViolations} violation(s): ${auditResult.violations.map((v) => v.id).join(", ")}` : "No accessibility violations found"
|
|
20863
|
+
};
|
|
20864
|
+
} catch (err) {
|
|
20865
|
+
return {
|
|
20866
|
+
assertion,
|
|
20867
|
+
passed: false,
|
|
20868
|
+
actual: "",
|
|
20869
|
+
error: err instanceof Error ? err.message : String(err)
|
|
20870
|
+
};
|
|
20871
|
+
}
|
|
20872
|
+
}
|
|
20873
|
+
case "url_contains": {
|
|
20874
|
+
const url = page.url();
|
|
20875
|
+
const expected = String(assertion.expected ?? "");
|
|
20876
|
+
return {
|
|
20877
|
+
assertion,
|
|
20878
|
+
passed: url.includes(expected),
|
|
20879
|
+
actual: url
|
|
20880
|
+
};
|
|
20881
|
+
}
|
|
20882
|
+
case "title_contains": {
|
|
20883
|
+
const title = await page.title();
|
|
20884
|
+
const expected = String(assertion.expected ?? "");
|
|
20885
|
+
return {
|
|
20886
|
+
assertion,
|
|
20887
|
+
passed: title.includes(expected),
|
|
20888
|
+
actual: title
|
|
20889
|
+
};
|
|
20890
|
+
}
|
|
20891
|
+
case "cookie_exists": {
|
|
20892
|
+
const cookieName = assertion.expected;
|
|
20893
|
+
const cookies = await page.context().cookies();
|
|
20894
|
+
const found = cookies.some((c) => c.name === cookieName);
|
|
20895
|
+
return {
|
|
20896
|
+
assertion,
|
|
20897
|
+
passed: found,
|
|
20898
|
+
actual: found ? `Cookie "${cookieName}" exists` : `Cookie "${cookieName}" not found`
|
|
20899
|
+
};
|
|
20900
|
+
}
|
|
20901
|
+
case "cookie_not_exists": {
|
|
20902
|
+
const cookieName = assertion.expected;
|
|
20903
|
+
const cookies = await page.context().cookies();
|
|
20904
|
+
const found = cookies.some((c) => c.name === cookieName);
|
|
20905
|
+
return {
|
|
20906
|
+
assertion,
|
|
20907
|
+
passed: !found,
|
|
20908
|
+
actual: found ? `Cookie "${cookieName}" found (unexpected)` : `Cookie "${cookieName}" does not exist`
|
|
20909
|
+
};
|
|
20910
|
+
}
|
|
20911
|
+
case "cookie_value": {
|
|
20912
|
+
const [cookieName, expectedValue] = assertion.expected.split("=", 2);
|
|
20913
|
+
const cookies = await page.context().cookies();
|
|
20914
|
+
const cookie = cookies.find((c) => c.name === cookieName);
|
|
20915
|
+
const actualValue = cookie?.value ?? "";
|
|
20916
|
+
return {
|
|
20917
|
+
assertion,
|
|
20918
|
+
passed: actualValue === expectedValue,
|
|
20919
|
+
actual: cookie ? `${cookieName}=${actualValue}` : `Cookie "${cookieName}" not found`
|
|
20920
|
+
};
|
|
20921
|
+
}
|
|
20922
|
+
case "local_storage_exists": {
|
|
20923
|
+
const key = assertion.expected;
|
|
20924
|
+
const value = await page.evaluate((k) => localStorage.getItem(k), key);
|
|
20925
|
+
return {
|
|
20926
|
+
assertion,
|
|
20927
|
+
passed: value !== null,
|
|
20928
|
+
actual: value !== null ? `Key "${key}" exists with value "${value}"` : `Key "${key}" not found in localStorage`
|
|
20929
|
+
};
|
|
20930
|
+
}
|
|
20931
|
+
case "local_storage_not_exists": {
|
|
20932
|
+
const key = assertion.expected;
|
|
20933
|
+
const value = await page.evaluate((k) => localStorage.getItem(k), key);
|
|
20934
|
+
return {
|
|
20935
|
+
assertion,
|
|
20936
|
+
passed: value === null,
|
|
20937
|
+
actual: value !== null ? `Key "${key}" exists (unexpected)` : `Key "${key}" does not exist in localStorage`
|
|
20938
|
+
};
|
|
20939
|
+
}
|
|
20940
|
+
case "local_storage_value": {
|
|
20941
|
+
const [lsKey, expectedValue] = assertion.expected.split("=", 2);
|
|
20942
|
+
const value = await page.evaluate((k) => localStorage.getItem(k), lsKey ?? "");
|
|
20943
|
+
return {
|
|
20944
|
+
assertion,
|
|
20945
|
+
passed: value === expectedValue,
|
|
20946
|
+
actual: value !== null ? `${lsKey}=${value}` : `Key "${lsKey}" not found in localStorage`
|
|
20947
|
+
};
|
|
20948
|
+
}
|
|
20949
|
+
case "session_storage_value": {
|
|
20950
|
+
const [ssKey, expectedValue] = assertion.expected.split("=", 2);
|
|
20951
|
+
const value = await page.evaluate((k) => sessionStorage.getItem(k), ssKey ?? "");
|
|
20952
|
+
return {
|
|
20953
|
+
assertion,
|
|
20954
|
+
passed: value === expectedValue,
|
|
20955
|
+
actual: value !== null ? `${ssKey}=${value}` : `Key "${ssKey}" not found in sessionStorage`
|
|
20956
|
+
};
|
|
20957
|
+
}
|
|
20958
|
+
case "session_storage_not_exists": {
|
|
20959
|
+
const key = assertion.expected;
|
|
20960
|
+
const value = await page.evaluate((k) => sessionStorage.getItem(k), key);
|
|
20961
|
+
return {
|
|
20962
|
+
assertion,
|
|
20963
|
+
passed: value === null,
|
|
20964
|
+
actual: value !== null ? `Key "${key}" exists (unexpected)` : `Key "${key}" does not exist in sessionStorage`
|
|
20965
|
+
};
|
|
20966
|
+
}
|
|
20967
|
+
default: {
|
|
20968
|
+
return {
|
|
20969
|
+
assertion,
|
|
20970
|
+
passed: false,
|
|
20971
|
+
actual: "",
|
|
20972
|
+
error: `Unknown assertion type: ${assertion.type}`
|
|
20973
|
+
};
|
|
20974
|
+
}
|
|
20975
|
+
}
|
|
20976
|
+
}
|
|
20977
|
+
function allAssertionsPassed(results) {
|
|
20978
|
+
return results.every((r) => r.passed);
|
|
20979
|
+
}
|
|
20980
|
+
function formatAssertionResults(results) {
|
|
20981
|
+
if (results.length === 0)
|
|
20982
|
+
return "No assertions.";
|
|
20983
|
+
const lines = [];
|
|
20984
|
+
for (const r of results) {
|
|
20985
|
+
const icon = r.passed ? "PASS" : "FAIL";
|
|
20986
|
+
const desc = r.assertion.description || `${r.assertion.type}${r.assertion.selector ? ` ${r.assertion.selector}` : ""}`;
|
|
20987
|
+
let line = ` [${icon}] ${desc}`;
|
|
20988
|
+
if (!r.passed) {
|
|
20989
|
+
line += ` (actual: ${r.actual})`;
|
|
20990
|
+
if (r.error)
|
|
20991
|
+
line += ` \u2014 ${r.error}`;
|
|
20992
|
+
}
|
|
20993
|
+
lines.push(line);
|
|
20994
|
+
}
|
|
20995
|
+
const passed = results.filter((r) => r.passed).length;
|
|
20996
|
+
lines.push(`
|
|
20997
|
+
${passed}/${results.length} assertions passed.`);
|
|
20998
|
+
return lines.join(`
|
|
20999
|
+
`);
|
|
21000
|
+
}
|
|
21001
|
+
var init_assertions = () => {};
|
|
21002
|
+
|
|
20627
21003
|
// src/db/flows.ts
|
|
20628
21004
|
var exports_flows = {};
|
|
20629
21005
|
__export(exports_flows, {
|
|
@@ -20782,7 +21158,9 @@ __export(exports_runner, {
|
|
|
20782
21158
|
runSingleScenario: () => runSingleScenario,
|
|
20783
21159
|
runByFilter: () => runByFilter,
|
|
20784
21160
|
runBatch: () => runBatch,
|
|
20785
|
-
|
|
21161
|
+
resolveScenariosForRun: () => resolveScenariosForRun,
|
|
21162
|
+
onRunEvent: () => onRunEvent,
|
|
21163
|
+
applyStructuredAssertionsToResult: () => applyStructuredAssertionsToResult
|
|
20786
21164
|
});
|
|
20787
21165
|
import { mkdirSync as mkdirSync8 } from "fs";
|
|
20788
21166
|
import { join as join13 } from "path";
|
|
@@ -20794,6 +21172,54 @@ function emit(event) {
|
|
|
20794
21172
|
if (eventHandler)
|
|
20795
21173
|
eventHandler(event);
|
|
20796
21174
|
}
|
|
21175
|
+
function assertionDescription(result) {
|
|
21176
|
+
return result.assertion.description || `${result.assertion.type}${result.assertion.selector ? ` ${result.assertion.selector}` : ""}`;
|
|
21177
|
+
}
|
|
21178
|
+
function summarizeAssertionResult(result) {
|
|
21179
|
+
const description = assertionDescription(result);
|
|
21180
|
+
if (result.passed)
|
|
21181
|
+
return description;
|
|
21182
|
+
const suffix = result.error ? `; ${result.error}` : "";
|
|
21183
|
+
return `${description} (actual: ${result.actual}${suffix})`;
|
|
21184
|
+
}
|
|
21185
|
+
async function applyStructuredAssertionsToResult(input) {
|
|
21186
|
+
const assertions = input.scenario.assertions ?? [];
|
|
21187
|
+
if (assertions.length === 0) {
|
|
21188
|
+
return {
|
|
21189
|
+
status: input.status,
|
|
21190
|
+
reasoning: input.reasoning,
|
|
21191
|
+
assertionsPassed: [],
|
|
21192
|
+
assertionsFailed: [],
|
|
21193
|
+
assertionResults: []
|
|
21194
|
+
};
|
|
21195
|
+
}
|
|
21196
|
+
const results = await evaluateAssertions(input.page, assertions, {
|
|
21197
|
+
consoleErrors: input.consoleErrors
|
|
21198
|
+
});
|
|
21199
|
+
const assertionsPassed = results.filter((r) => r.passed).map(summarizeAssertionResult);
|
|
21200
|
+
const assertionsFailed = results.filter((r) => !r.passed).map(summarizeAssertionResult);
|
|
21201
|
+
const assertionResults = results.map((result) => ({
|
|
21202
|
+
type: result.assertion.type,
|
|
21203
|
+
description: assertionDescription(result),
|
|
21204
|
+
passed: result.passed,
|
|
21205
|
+
actual: result.actual,
|
|
21206
|
+
...result.error ? { error: result.error } : {}
|
|
21207
|
+
}));
|
|
21208
|
+
const assertionsOk = allAssertionsPassed(results);
|
|
21209
|
+
const status = assertionsOk || input.status !== "passed" ? input.status : "failed";
|
|
21210
|
+
const assertionHeading = assertionsOk ? "Structured assertions passed:" : "Structured assertions failed:";
|
|
21211
|
+
const reasoningParts = [input.reasoning, `${assertionHeading}
|
|
21212
|
+
${formatAssertionResults(results)}`].map((part) => part.trim()).filter(Boolean);
|
|
21213
|
+
return {
|
|
21214
|
+
status,
|
|
21215
|
+
reasoning: reasoningParts.join(`
|
|
21216
|
+
|
|
21217
|
+
`),
|
|
21218
|
+
assertionsPassed,
|
|
21219
|
+
assertionsFailed,
|
|
21220
|
+
assertionResults
|
|
21221
|
+
};
|
|
21222
|
+
}
|
|
20797
21223
|
function withTimeout(promise, ms, label) {
|
|
20798
21224
|
return new Promise((resolve, reject) => {
|
|
20799
21225
|
const warningAt = Math.floor(ms * 0.8);
|
|
@@ -20964,6 +21390,7 @@ async function runSingleScenario(scenario, runId, options) {
|
|
|
20964
21390
|
model,
|
|
20965
21391
|
runId,
|
|
20966
21392
|
sessionId: result.id,
|
|
21393
|
+
baseUrl: options.url,
|
|
20967
21394
|
maxTurns: effectiveOptions.minimal ? 10 : 30,
|
|
20968
21395
|
a11y: effectiveOptions.a11y,
|
|
20969
21396
|
persona: persona ? {
|
|
@@ -21046,27 +21473,46 @@ async function runSingleScenario(scenario, runId, options) {
|
|
|
21046
21473
|
closeSession(result.id);
|
|
21047
21474
|
const lightpandaNote = options.engine === "lightpanda" ? " (Running with Lightpanda \u2014 no screenshots)" : options.engine === "bun" ? " (Running with Bun.WebView \u2014 native, ~11x faster)" : "";
|
|
21048
21475
|
const networkMeta = networkErrors.length > 0 ? { networkErrors: networkErrors.slice(0, 20) } : {};
|
|
21049
|
-
|
|
21476
|
+
const baseReasoning = agentResult.reasoning ? agentResult.reasoning + lightpandaNote : lightpandaNote || "";
|
|
21477
|
+
const assertionOutcome = await applyStructuredAssertionsToResult({
|
|
21478
|
+
page,
|
|
21479
|
+
scenario,
|
|
21480
|
+
consoleErrors,
|
|
21050
21481
|
status: agentResult.status,
|
|
21051
|
-
reasoning:
|
|
21482
|
+
reasoning: baseReasoning
|
|
21483
|
+
});
|
|
21484
|
+
const structuredAssertionMeta = assertionOutcome.assertionResults.length > 0 ? {
|
|
21485
|
+
structuredAssertions: {
|
|
21486
|
+
passed: assertionOutcome.assertionsPassed,
|
|
21487
|
+
failed: assertionOutcome.assertionsFailed,
|
|
21488
|
+
results: assertionOutcome.assertionResults
|
|
21489
|
+
}
|
|
21490
|
+
} : {};
|
|
21491
|
+
let updatedResult = updateResult(result.id, {
|
|
21492
|
+
status: assertionOutcome.status,
|
|
21493
|
+
reasoning: assertionOutcome.reasoning || undefined,
|
|
21052
21494
|
stepsCompleted: agentResult.stepsCompleted,
|
|
21053
21495
|
durationMs: Date.now() - new Date(result.createdAt).getTime(),
|
|
21054
21496
|
tokensUsed: agentResult.tokensUsed,
|
|
21055
21497
|
costCents: estimateCost(model, agentResult.tokensUsed),
|
|
21056
|
-
metadata: {
|
|
21498
|
+
metadata: {
|
|
21499
|
+
consoleLogs,
|
|
21500
|
+
...networkErrors.length > 0 ? networkMeta : {},
|
|
21501
|
+
...structuredAssertionMeta
|
|
21502
|
+
}
|
|
21057
21503
|
});
|
|
21058
|
-
if (
|
|
21059
|
-
const failureAnalysis = analyzeFailure(null,
|
|
21504
|
+
if (assertionOutcome.status === "failed" || assertionOutcome.status === "error") {
|
|
21505
|
+
const failureAnalysis = analyzeFailure(null, assertionOutcome.reasoning ?? null);
|
|
21060
21506
|
if (failureAnalysis) {
|
|
21061
21507
|
updatedResult = updateResult(result.id, { failureAnalysis });
|
|
21062
21508
|
}
|
|
21063
21509
|
}
|
|
21064
|
-
if (
|
|
21510
|
+
if (assertionOutcome.status === "passed") {
|
|
21065
21511
|
try {
|
|
21066
21512
|
updateScenarioPassedCache(scenario.id, options.url);
|
|
21067
21513
|
} catch {}
|
|
21068
21514
|
}
|
|
21069
|
-
const eventType =
|
|
21515
|
+
const eventType = assertionOutcome.status === "passed" ? "scenario:pass" : "scenario:fail";
|
|
21070
21516
|
emit({ type: eventType, scenarioId: scenario.id, scenarioName: scenario.name, resultId: result.id, runId });
|
|
21071
21517
|
return updatedResult;
|
|
21072
21518
|
} catch (error) {
|
|
@@ -21091,7 +21537,8 @@ async function runSingleScenario(scenario, runId, options) {
|
|
|
21091
21537
|
} finally {
|
|
21092
21538
|
if (harPath) {
|
|
21093
21539
|
try {
|
|
21094
|
-
|
|
21540
|
+
const existing = getResult(result.id);
|
|
21541
|
+
updateResult(result.id, { metadata: { ...existing?.metadata ?? {}, harPath } });
|
|
21095
21542
|
} catch {}
|
|
21096
21543
|
}
|
|
21097
21544
|
if (browser) {
|
|
@@ -21263,22 +21710,31 @@ async function runBatch(scenarios, options) {
|
|
|
21263
21710
|
}
|
|
21264
21711
|
return { run: finalRun, results };
|
|
21265
21712
|
}
|
|
21266
|
-
|
|
21267
|
-
|
|
21713
|
+
function findScenarioInList(scenarios, id) {
|
|
21714
|
+
return scenarios.find((scenario) => scenario.id === id || scenario.shortId === id || scenario.id.startsWith(id)) ?? null;
|
|
21715
|
+
}
|
|
21716
|
+
function resolveScenariosForRun(options) {
|
|
21268
21717
|
if (options.scenarioIds && options.scenarioIds.length > 0) {
|
|
21269
|
-
const
|
|
21270
|
-
|
|
21271
|
-
|
|
21272
|
-
|
|
21273
|
-
|
|
21718
|
+
const scoped = listScenarios({ projectId: options.projectId });
|
|
21719
|
+
const resolved = [];
|
|
21720
|
+
const seen = new Set;
|
|
21721
|
+
for (const id of options.scenarioIds) {
|
|
21722
|
+
const scenario = findScenarioInList(scoped, id) ?? getScenario(id);
|
|
21723
|
+
if (scenario && !seen.has(scenario.id)) {
|
|
21724
|
+
resolved.push(scenario);
|
|
21725
|
+
seen.add(scenario.id);
|
|
21726
|
+
}
|
|
21274
21727
|
}
|
|
21275
|
-
|
|
21276
|
-
scenarios = listScenarios({
|
|
21277
|
-
projectId: options.projectId,
|
|
21278
|
-
tags: options.tags,
|
|
21279
|
-
priority: options.priority
|
|
21280
|
-
});
|
|
21728
|
+
return resolved;
|
|
21281
21729
|
}
|
|
21730
|
+
return listScenarios({
|
|
21731
|
+
projectId: options.projectId,
|
|
21732
|
+
tags: options.tags,
|
|
21733
|
+
priority: options.priority
|
|
21734
|
+
});
|
|
21735
|
+
}
|
|
21736
|
+
async function runByFilter(options) {
|
|
21737
|
+
const scenarios = resolveScenariosForRun(options);
|
|
21282
21738
|
if (scenarios.length === 0) {
|
|
21283
21739
|
const config = loadConfig();
|
|
21284
21740
|
const model = resolveModel2(options.model ?? config.defaultModel);
|
|
@@ -21291,17 +21747,7 @@ async function runByFilter(options) {
|
|
|
21291
21747
|
function startRunAsync(options) {
|
|
21292
21748
|
const config = loadConfig();
|
|
21293
21749
|
const model = resolveModel2(options.model ?? config.defaultModel);
|
|
21294
|
-
|
|
21295
|
-
if (options.scenarioIds && options.scenarioIds.length > 0) {
|
|
21296
|
-
const all = listScenarios({ projectId: options.projectId });
|
|
21297
|
-
scenarios = all.filter((s) => options.scenarioIds.includes(s.id) || options.scenarioIds.includes(s.shortId));
|
|
21298
|
-
} else {
|
|
21299
|
-
scenarios = listScenarios({
|
|
21300
|
-
projectId: options.projectId,
|
|
21301
|
-
tags: options.tags,
|
|
21302
|
-
priority: options.priority
|
|
21303
|
-
});
|
|
21304
|
-
}
|
|
21750
|
+
const scenarios = resolveScenariosForRun(options);
|
|
21305
21751
|
if (!options.skipBudgetCheck) {
|
|
21306
21752
|
const cap = options.maxCostCents ?? config.defaultMaxCostCents;
|
|
21307
21753
|
if (cap !== undefined && cap > 0 && scenarios.length > 0) {
|
|
@@ -21405,6 +21851,7 @@ var init_runner = __esm(() => {
|
|
|
21405
21851
|
init_session_tracker();
|
|
21406
21852
|
init_webhooks();
|
|
21407
21853
|
init_failure_pipeline();
|
|
21854
|
+
init_assertions();
|
|
21408
21855
|
});
|
|
21409
21856
|
|
|
21410
21857
|
// src/lib/affected.ts
|
|
@@ -22879,18 +23326,7 @@ function normalizeFilter(input) {
|
|
|
22879
23326
|
};
|
|
22880
23327
|
}
|
|
22881
23328
|
function normalizeExecution(input) {
|
|
22882
|
-
|
|
22883
|
-
if (target === "connector:e2b") {
|
|
22884
|
-
return {
|
|
22885
|
-
target,
|
|
22886
|
-
connector: input?.connector ?? "e2b",
|
|
22887
|
-
operation: input?.operation ?? "run",
|
|
22888
|
-
sandboxTemplate: input?.sandboxTemplate,
|
|
22889
|
-
timeoutMs: input?.timeoutMs,
|
|
22890
|
-
env: input?.env
|
|
22891
|
-
};
|
|
22892
|
-
}
|
|
22893
|
-
return { ...DEFAULT_EXECUTION, timeoutMs: input?.timeoutMs };
|
|
23329
|
+
return input ? workflowExecutionFromValue(input) : DEFAULT_EXECUTION;
|
|
22894
23330
|
}
|
|
22895
23331
|
function createTestingWorkflow(input) {
|
|
22896
23332
|
const db2 = getDatabase();
|
|
@@ -22941,6 +23377,9 @@ var init_workflows = __esm(() => {
|
|
|
22941
23377
|
});
|
|
22942
23378
|
|
|
22943
23379
|
// src/lib/workflow-runner.ts
|
|
23380
|
+
import { mkdtempSync, rmSync, writeFileSync as writeFileSync3 } from "fs";
|
|
23381
|
+
import { tmpdir } from "os";
|
|
23382
|
+
import { join as join14 } from "path";
|
|
22944
23383
|
function buildWorkflowRunPlan(workflow, options) {
|
|
22945
23384
|
const runOptions = {
|
|
22946
23385
|
url: options.url,
|
|
@@ -22957,10 +23396,10 @@ function buildWorkflowRunPlan(workflow, options) {
|
|
|
22957
23396
|
return {
|
|
22958
23397
|
workflow,
|
|
22959
23398
|
runOptions,
|
|
22960
|
-
|
|
23399
|
+
sandbox: workflow.execution.target === "sandbox" ? buildSandboxPlan(workflow, workflow.execution, runOptions) : null
|
|
22961
23400
|
};
|
|
22962
23401
|
}
|
|
22963
|
-
async function runTestingWorkflow(workflowId, options) {
|
|
23402
|
+
async function runTestingWorkflow(workflowId, options, dependencies = {}) {
|
|
22964
23403
|
const workflow = getTestingWorkflow(workflowId);
|
|
22965
23404
|
if (!workflow)
|
|
22966
23405
|
throw new Error(`Testing workflow not found: ${workflowId}`);
|
|
@@ -22970,13 +23409,25 @@ async function runTestingWorkflow(workflowId, options) {
|
|
|
22970
23409
|
const plan = buildWorkflowRunPlan(workflow, options);
|
|
22971
23410
|
if (options.dryRun)
|
|
22972
23411
|
return { run: null, results: [], plan };
|
|
22973
|
-
if (workflow.execution.target === "
|
|
22974
|
-
const
|
|
22975
|
-
return { run: null, results: [], plan,
|
|
23412
|
+
if (workflow.execution.target === "sandbox") {
|
|
23413
|
+
const sandboxResult = await runViaSandbox(plan, dependencies);
|
|
23414
|
+
return { run: null, results: [], plan, sandboxResult };
|
|
22976
23415
|
}
|
|
22977
|
-
const
|
|
23416
|
+
const runLocal = dependencies.runByFilter ?? runByFilter;
|
|
23417
|
+
const { run, results } = await runLocal(plan.runOptions);
|
|
22978
23418
|
return { run, results, plan };
|
|
22979
23419
|
}
|
|
23420
|
+
function createWorkflowDatabaseBundle(workflow, plan) {
|
|
23421
|
+
if (!plan.sandbox)
|
|
23422
|
+
throw new Error(`Workflow is not configured for sandbox execution: ${workflow.name}`);
|
|
23423
|
+
const localDir = mkdtempSync(join14(tmpdir(), `testers-workflow-${workflow.id.slice(0, 8)}-`));
|
|
23424
|
+
writeFileSync3(join14(localDir, "testers.db"), getDatabase().serialize());
|
|
23425
|
+
return {
|
|
23426
|
+
localDir,
|
|
23427
|
+
remoteDir: plan.sandbox.stateRemoteDir,
|
|
23428
|
+
cleanup: () => rmSync(localDir, { recursive: true, force: true })
|
|
23429
|
+
};
|
|
23430
|
+
}
|
|
22980
23431
|
function validatePersonaIds(workflow) {
|
|
22981
23432
|
for (const personaId of workflow.personaIds) {
|
|
22982
23433
|
if (!getPersona(personaId)) {
|
|
@@ -22984,48 +23435,112 @@ function validatePersonaIds(workflow) {
|
|
|
22984
23435
|
}
|
|
22985
23436
|
}
|
|
22986
23437
|
}
|
|
22987
|
-
function
|
|
22988
|
-
const
|
|
22989
|
-
const
|
|
22990
|
-
|
|
22991
|
-
|
|
22992
|
-
|
|
23438
|
+
function buildSandboxPlan(workflow, execution, runOptions) {
|
|
23439
|
+
const remoteDir = execution.sandboxRemoteDir ?? `/tmp/testers-workflow-${workflow.id.slice(0, 8)}`;
|
|
23440
|
+
const stateRemoteDir = `${remoteDir.replace(/\/+$/, "")}/.testers-state`;
|
|
23441
|
+
return {
|
|
23442
|
+
provider: execution.provider,
|
|
23443
|
+
image: execution.sandboxImage,
|
|
23444
|
+
name: `testers-${workflow.id.slice(0, 8)}`,
|
|
23445
|
+
remoteDir,
|
|
23446
|
+
stateRemoteDir,
|
|
23447
|
+
cleanup: execution.sandboxCleanup ?? "delete",
|
|
22993
23448
|
timeoutMs: execution.timeoutMs,
|
|
22994
|
-
env: execution.env
|
|
22995
|
-
command:
|
|
22996
|
-
|
|
22997
|
-
|
|
22998
|
-
|
|
22999
|
-
|
|
23000
|
-
|
|
23001
|
-
|
|
23002
|
-
|
|
23003
|
-
...runOptions.projectId ? ["--project", runOptions.projectId] : [],
|
|
23004
|
-
...runOptions.model ? ["--model", runOptions.model] : [],
|
|
23005
|
-
"--json"
|
|
23006
|
-
]
|
|
23007
|
-
});
|
|
23008
|
-
return ["connectors", "run", connector, operation, payload];
|
|
23449
|
+
env: execution.env,
|
|
23450
|
+
command: buildSandboxCommand({
|
|
23451
|
+
runOptions,
|
|
23452
|
+
remoteDir,
|
|
23453
|
+
dbPath: `${stateRemoteDir}/testers.db`,
|
|
23454
|
+
setupCommand: execution.setupCommand,
|
|
23455
|
+
packageSpec: execution.packageSpec ?? "@hasna/testers"
|
|
23456
|
+
})
|
|
23457
|
+
};
|
|
23009
23458
|
}
|
|
23010
|
-
|
|
23011
|
-
|
|
23012
|
-
|
|
23013
|
-
|
|
23014
|
-
|
|
23015
|
-
|
|
23016
|
-
|
|
23017
|
-
|
|
23018
|
-
|
|
23019
|
-
|
|
23020
|
-
|
|
23021
|
-
|
|
23022
|
-
|
|
23023
|
-
|
|
23024
|
-
|
|
23459
|
+
function buildSandboxCommand(input) {
|
|
23460
|
+
const args = [
|
|
23461
|
+
"bunx",
|
|
23462
|
+
input.packageSpec,
|
|
23463
|
+
"run",
|
|
23464
|
+
input.runOptions.url,
|
|
23465
|
+
...input.runOptions.scenarioIds?.length ? ["--scenario", input.runOptions.scenarioIds.join(",")] : [],
|
|
23466
|
+
...input.runOptions.tags?.length ? input.runOptions.tags.flatMap((tag) => ["--tag", tag]) : [],
|
|
23467
|
+
...input.runOptions.priority ? ["--priority", input.runOptions.priority] : [],
|
|
23468
|
+
...input.runOptions.projectId ? ["--project", input.runOptions.projectId] : [],
|
|
23469
|
+
...input.runOptions.model ? ["--model", input.runOptions.model] : [],
|
|
23470
|
+
...input.runOptions.headed ? ["--headed"] : [],
|
|
23471
|
+
...input.runOptions.parallel ? ["--parallel", String(input.runOptions.parallel)] : [],
|
|
23472
|
+
...input.runOptions.timeout ? ["--timeout", String(input.runOptions.timeout)] : [],
|
|
23473
|
+
...input.runOptions.personaIds?.length ? ["--persona", input.runOptions.personaIds.join(",")] : [],
|
|
23474
|
+
"--no-auto-generate",
|
|
23475
|
+
"--json"
|
|
23476
|
+
];
|
|
23477
|
+
return [
|
|
23478
|
+
"set -euo pipefail",
|
|
23479
|
+
`mkdir -p ${shellQuote(input.remoteDir)}`,
|
|
23480
|
+
`cd ${shellQuote(input.remoteDir)}`,
|
|
23481
|
+
input.setupCommand,
|
|
23482
|
+
`HASNA_TESTERS_DB_PATH=${shellQuote(input.dbPath)} ${args.map(shellQuote).join(" ")}`
|
|
23483
|
+
].filter(Boolean).join(`
|
|
23484
|
+
`);
|
|
23485
|
+
}
|
|
23486
|
+
async function runViaSandbox(plan, dependencies) {
|
|
23487
|
+
if (!plan.sandbox)
|
|
23488
|
+
throw new Error("Workflow does not have a sandbox plan");
|
|
23489
|
+
const sandboxes = await resolveSandboxesRuntime(dependencies);
|
|
23490
|
+
const createBundle = dependencies.createDatabaseBundle ?? createWorkflowDatabaseBundle;
|
|
23491
|
+
const bundle = createBundle(plan.workflow, plan);
|
|
23492
|
+
try {
|
|
23493
|
+
const raw = await sandboxes.runCommandInSandbox({
|
|
23494
|
+
command: plan.sandbox.command,
|
|
23495
|
+
provider: plan.sandbox.provider,
|
|
23496
|
+
name: plan.sandbox.name,
|
|
23497
|
+
image: plan.sandbox.image,
|
|
23498
|
+
sandboxTimeout: plan.sandbox.timeoutMs,
|
|
23499
|
+
commandTimeoutMs: plan.sandbox.timeoutMs,
|
|
23500
|
+
projectId: plan.workflow.projectId ?? undefined,
|
|
23501
|
+
config: {
|
|
23502
|
+
source: "testers",
|
|
23503
|
+
workflowId: plan.workflow.id,
|
|
23504
|
+
workflowName: plan.workflow.name
|
|
23505
|
+
},
|
|
23506
|
+
sandboxEnvVars: plan.sandbox.env,
|
|
23507
|
+
cleanup: plan.sandbox.cleanup,
|
|
23508
|
+
upload: {
|
|
23509
|
+
localDir: bundle.localDir,
|
|
23510
|
+
remoteDir: bundle.remoteDir
|
|
23511
|
+
}
|
|
23512
|
+
});
|
|
23513
|
+
const exitCode = raw.result.exit_code ?? raw.result.exitCode ?? 0;
|
|
23514
|
+
const stdout = raw.result.stdout ?? "";
|
|
23515
|
+
const stderr = raw.result.stderr ?? "";
|
|
23516
|
+
if (exitCode !== 0) {
|
|
23517
|
+
throw new Error(`Sandbox workflow execution failed (${exitCode}): ${stderr || stdout}`);
|
|
23518
|
+
}
|
|
23519
|
+
return {
|
|
23520
|
+
sandboxId: raw.sandbox.id,
|
|
23521
|
+
sessionId: raw.session.id,
|
|
23522
|
+
exitCode,
|
|
23523
|
+
stdout,
|
|
23524
|
+
stderr,
|
|
23525
|
+
cleanup: raw.cleanup
|
|
23526
|
+
};
|
|
23527
|
+
} finally {
|
|
23528
|
+
bundle.cleanup?.();
|
|
23025
23529
|
}
|
|
23026
|
-
|
|
23530
|
+
}
|
|
23531
|
+
async function resolveSandboxesRuntime(dependencies) {
|
|
23532
|
+
if (dependencies.sandboxes)
|
|
23533
|
+
return dependencies.sandboxes;
|
|
23534
|
+
if (dependencies.createSandboxesSDK)
|
|
23535
|
+
return dependencies.createSandboxesSDK();
|
|
23536
|
+
const mod = await import("@hasna/sandboxes");
|
|
23537
|
+
return mod.createSandboxesSDK();
|
|
23538
|
+
}
|
|
23539
|
+
function shellQuote(value) {
|
|
23540
|
+
return `'${value.replaceAll("'", `'"'"'`)}'`;
|
|
23027
23541
|
}
|
|
23028
23542
|
var init_workflow_runner = __esm(() => {
|
|
23543
|
+
init_database();
|
|
23029
23544
|
init_workflows();
|
|
23030
23545
|
init_personas();
|
|
23031
23546
|
init_runner();
|
|
@@ -53049,11 +53564,11 @@ import { exec } from "child_process";
|
|
|
53049
53564
|
import { promisify } from "util";
|
|
53050
53565
|
import { readFileSync as readFileSync3 } from "fs";
|
|
53051
53566
|
import { webcrypto as crypto2 } from "crypto";
|
|
53052
|
-
import { existsSync as existsSync42, writeFileSync as
|
|
53567
|
+
import { existsSync as existsSync42, writeFileSync as writeFileSync32, readFileSync as readFileSync22, mkdirSync as mkdirSync32 } from "fs";
|
|
53053
53568
|
import { join as join42 } from "path";
|
|
53054
53569
|
import { Database as Database4 } from "bun:sqlite";
|
|
53055
53570
|
import { existsSync as existsSync11, mkdirSync as mkdirSync9 } from "fs";
|
|
53056
|
-
import { dirname as dirname4, join as
|
|
53571
|
+
import { dirname as dirname4, join as join15, resolve as resolve2 } from "path";
|
|
53057
53572
|
import { existsSync as existsSync22, writeFileSync as writeFileSync4 } from "fs";
|
|
53058
53573
|
import { join as join22 } from "path";
|
|
53059
53574
|
import { execSync as execSync2, execFileSync } from "child_process";
|
|
@@ -53228,7 +53743,7 @@ function getDbPath2() {
|
|
|
53228
53743
|
return process.env["PROJECTS_DB_PATH"];
|
|
53229
53744
|
}
|
|
53230
53745
|
const home = process.env["HOME"] || process.env["USERPROFILE"] || "~";
|
|
53231
|
-
return
|
|
53746
|
+
return join15(home, ".hasna", "projects", "projects.db");
|
|
53232
53747
|
}
|
|
53233
53748
|
function ensureDir2(filePath) {
|
|
53234
53749
|
if (filePath === ":memory:")
|
|
@@ -53516,7 +54031,7 @@ function setIntegrations(id, integrations, db2) {
|
|
|
53516
54031
|
const jsonPath = join42(project.path, ".project.json");
|
|
53517
54032
|
if (existsSync42(jsonPath)) {
|
|
53518
54033
|
const existing = JSON.parse(readFileSync22(jsonPath, "utf-8"));
|
|
53519
|
-
|
|
54034
|
+
writeFileSync32(jsonPath, JSON.stringify({ ...existing, integrations: merged }, null, 2) + `
|
|
53520
54035
|
`, "utf-8");
|
|
53521
54036
|
}
|
|
53522
54037
|
} catch {}
|
|
@@ -69369,11 +69884,11 @@ More information can be found at: https://a.co/c895JFp`);
|
|
|
69369
69884
|
var numberSelector = (obj, key, type2) => {
|
|
69370
69885
|
if (!(key in obj))
|
|
69371
69886
|
return;
|
|
69372
|
-
const
|
|
69373
|
-
if (Number.isNaN(
|
|
69887
|
+
const numberValue2 = parseInt(obj[key], 10);
|
|
69888
|
+
if (Number.isNaN(numberValue2)) {
|
|
69374
69889
|
throw new TypeError(`Cannot load ${type2} '${key}'. Expected number, got '${obj[key]}'.`);
|
|
69375
69890
|
}
|
|
69376
|
-
return
|
|
69891
|
+
return numberValue2;
|
|
69377
69892
|
};
|
|
69378
69893
|
exports.SelectorType = undefined;
|
|
69379
69894
|
(function(SelectorType2) {
|
|
@@ -84853,10 +85368,10 @@ __export(exports_contacts_connector, {
|
|
|
84853
85368
|
function getContactsDb() {
|
|
84854
85369
|
const { Database: Database5 } = __require("bun:sqlite");
|
|
84855
85370
|
const { existsSync: existsSync5 } = __require("fs");
|
|
84856
|
-
const { join:
|
|
85371
|
+
const { join: join16 } = __require("path");
|
|
84857
85372
|
const { homedir: homedir7 } = __require("os");
|
|
84858
85373
|
const envPath = process.env["HASNA_CONTACTS_DB_PATH"] ?? process.env["OPEN_CONTACTS_DB"];
|
|
84859
|
-
const dbPath = envPath ??
|
|
85374
|
+
const dbPath = envPath ?? join16(homedir7(), ".hasna", "contacts", "contacts.db");
|
|
84860
85375
|
if (!existsSync5(dbPath))
|
|
84861
85376
|
return null;
|
|
84862
85377
|
const db2 = new Database5(dbPath, { readonly: true });
|
|
@@ -84977,7 +85492,7 @@ __export(exports_army_runner, {
|
|
|
84977
85492
|
waitForArmyRun: () => waitForArmyRun,
|
|
84978
85493
|
runWithArmy: () => runWithArmy
|
|
84979
85494
|
});
|
|
84980
|
-
import { join as
|
|
85495
|
+
import { join as join16 } from "path";
|
|
84981
85496
|
function chunkArray(arr, n2) {
|
|
84982
85497
|
const chunks = [];
|
|
84983
85498
|
const size = Math.ceil(arr.length / n2);
|
|
@@ -84987,7 +85502,7 @@ function chunkArray(arr, n2) {
|
|
|
84987
85502
|
return chunks;
|
|
84988
85503
|
}
|
|
84989
85504
|
function getCliPath() {
|
|
84990
|
-
const srcPath =
|
|
85505
|
+
const srcPath = join16(import.meta.dir, "../cli/index.tsx");
|
|
84991
85506
|
return srcPath;
|
|
84992
85507
|
}
|
|
84993
85508
|
async function runWithArmy(options) {
|
|
@@ -85759,9 +86274,30 @@ function buildServer() {
|
|
|
85759
86274
|
goalPrompt: exports_external.string().optional().describe("Goal prompt for the AI SDK workflow agent"),
|
|
85760
86275
|
successCriteria: exports_external.array(exports_external.string()).optional().describe("Goal success criteria"),
|
|
85761
86276
|
maxIterations: exports_external.number().int().min(1).max(20).optional().describe("Max goal loop iterations"),
|
|
85762
|
-
executionTarget: exports_external.enum(["local", "connector:e2b"]).optional().describe("Run locally or through the
|
|
85763
|
-
|
|
85764
|
-
|
|
86277
|
+
executionTarget: exports_external.enum(["local", "sandbox", "connector:e2b"]).optional().describe("Run locally or through the sandboxes SDK"),
|
|
86278
|
+
sandboxProvider: exports_external.string().optional().describe("Sandbox provider: e2b, daytona, or modal"),
|
|
86279
|
+
sandboxImage: exports_external.string().optional().describe("Sandbox image/template"),
|
|
86280
|
+
sandboxRemoteDir: exports_external.string().optional().describe("Remote working directory for sandbox runs"),
|
|
86281
|
+
sandboxCleanup: exports_external.enum(["delete", "stop", "keep"]).optional().describe("Sandbox cleanup mode"),
|
|
86282
|
+
e2bTemplate: exports_external.string().optional().describe("Legacy alias for sandboxImage")
|
|
86283
|
+
}, async ({
|
|
86284
|
+
name: name21,
|
|
86285
|
+
description,
|
|
86286
|
+
projectId,
|
|
86287
|
+
scenarioIds,
|
|
86288
|
+
tags,
|
|
86289
|
+
priority,
|
|
86290
|
+
personaIds,
|
|
86291
|
+
goalPrompt,
|
|
86292
|
+
successCriteria,
|
|
86293
|
+
maxIterations,
|
|
86294
|
+
executionTarget,
|
|
86295
|
+
sandboxProvider,
|
|
86296
|
+
sandboxImage,
|
|
86297
|
+
sandboxRemoteDir,
|
|
86298
|
+
sandboxCleanup,
|
|
86299
|
+
e2bTemplate
|
|
86300
|
+
}) => {
|
|
85765
86301
|
try {
|
|
85766
86302
|
return json3(createTestingWorkflow({
|
|
85767
86303
|
name: name21,
|
|
@@ -85772,8 +86308,10 @@ function buildServer() {
|
|
|
85772
86308
|
goal: goalPrompt ? { prompt: goalPrompt, successCriteria, maxIterations } : null,
|
|
85773
86309
|
execution: {
|
|
85774
86310
|
target: executionTarget ?? "local",
|
|
85775
|
-
|
|
85776
|
-
|
|
86311
|
+
provider: sandboxProvider ?? (executionTarget === "connector:e2b" ? "e2b" : undefined),
|
|
86312
|
+
sandboxImage: sandboxImage ?? e2bTemplate,
|
|
86313
|
+
sandboxRemoteDir,
|
|
86314
|
+
sandboxCleanup
|
|
85777
86315
|
}
|
|
85778
86316
|
}));
|
|
85779
86317
|
} catch (error40) {
|