@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/server/index.js
CHANGED
|
@@ -4036,6 +4036,56 @@ var init_zod = __esm(() => {
|
|
|
4036
4036
|
});
|
|
4037
4037
|
|
|
4038
4038
|
// src/types/index.ts
|
|
4039
|
+
function isRecord(value) {
|
|
4040
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4041
|
+
}
|
|
4042
|
+
function stringValue(value) {
|
|
4043
|
+
return typeof value === "string" && value.trim() ? value : undefined;
|
|
4044
|
+
}
|
|
4045
|
+
function numberValue(value) {
|
|
4046
|
+
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
4047
|
+
}
|
|
4048
|
+
function stringMap(value) {
|
|
4049
|
+
if (!isRecord(value))
|
|
4050
|
+
return;
|
|
4051
|
+
const entries = Object.entries(value).filter((entry) => typeof entry[1] === "string");
|
|
4052
|
+
return entries.length > 0 ? Object.fromEntries(entries) : undefined;
|
|
4053
|
+
}
|
|
4054
|
+
function cleanupValue(value) {
|
|
4055
|
+
if (value === "delete" || value === "stop" || value === "keep")
|
|
4056
|
+
return value;
|
|
4057
|
+
return;
|
|
4058
|
+
}
|
|
4059
|
+
function workflowExecutionFromValue(value) {
|
|
4060
|
+
const input = isRecord(value) ? value : {};
|
|
4061
|
+
const rawTarget = stringValue(input["target"]) ?? "local";
|
|
4062
|
+
if (rawTarget === "local") {
|
|
4063
|
+
const timeoutMs2 = numberValue(input["timeoutMs"]);
|
|
4064
|
+
return timeoutMs2 === undefined ? { target: "local" } : { target: "local", timeoutMs: timeoutMs2 };
|
|
4065
|
+
}
|
|
4066
|
+
if (rawTarget !== "sandbox" && rawTarget !== "connector:e2b") {
|
|
4067
|
+
throw new Error(`Unsupported workflow execution target: ${rawTarget}`);
|
|
4068
|
+
}
|
|
4069
|
+
const provider = rawTarget === "connector:e2b" ? "e2b" : stringValue(input["provider"]) ?? stringValue(input["connector"]);
|
|
4070
|
+
const sandboxImage = stringValue(input["sandboxImage"]) ?? stringValue(input["sandboxTemplate"]);
|
|
4071
|
+
const sandboxRemoteDir = stringValue(input["sandboxRemoteDir"]);
|
|
4072
|
+
const sandboxCleanup = cleanupValue(input["sandboxCleanup"]);
|
|
4073
|
+
const setupCommand = stringValue(input["setupCommand"]);
|
|
4074
|
+
const packageSpec = stringValue(input["packageSpec"]);
|
|
4075
|
+
const timeoutMs = numberValue(input["timeoutMs"]);
|
|
4076
|
+
const env = stringMap(input["env"]);
|
|
4077
|
+
return {
|
|
4078
|
+
target: "sandbox",
|
|
4079
|
+
...provider ? { provider } : {},
|
|
4080
|
+
...sandboxImage ? { sandboxImage } : {},
|
|
4081
|
+
...sandboxRemoteDir ? { sandboxRemoteDir } : {},
|
|
4082
|
+
...sandboxCleanup ? { sandboxCleanup } : {},
|
|
4083
|
+
...setupCommand ? { setupCommand } : {},
|
|
4084
|
+
...packageSpec ? { packageSpec } : {},
|
|
4085
|
+
...timeoutMs !== undefined ? { timeoutMs } : {},
|
|
4086
|
+
...env ? { env } : {}
|
|
4087
|
+
};
|
|
4088
|
+
}
|
|
4039
4089
|
function workflowFromRow(row) {
|
|
4040
4090
|
return {
|
|
4041
4091
|
id: row.id,
|
|
@@ -4045,7 +4095,7 @@ function workflowFromRow(row) {
|
|
|
4045
4095
|
scenarioFilter: JSON.parse(row.scenario_filter || "{}"),
|
|
4046
4096
|
personaIds: JSON.parse(row.persona_ids || "[]"),
|
|
4047
4097
|
goal: row.goal ? JSON.parse(row.goal) : null,
|
|
4048
|
-
execution: JSON.parse(row.execution || '{"target":"local"}'),
|
|
4098
|
+
execution: workflowExecutionFromValue(JSON.parse(row.execution || '{"target":"local"}')),
|
|
4049
4099
|
settings: JSON.parse(row.settings || "{}"),
|
|
4050
4100
|
enabled: row.enabled === 1,
|
|
4051
4101
|
createdAt: row.created_at,
|
|
@@ -15811,7 +15861,6 @@ async function executeTool(page, screenshotter, toolName, toolInput, context) {
|
|
|
15811
15861
|
const assertionType = toolInput.assertion_type;
|
|
15812
15862
|
const selector = toolInput.selector;
|
|
15813
15863
|
const expected = toolInput.expected;
|
|
15814
|
-
const sessionId = context.sessionId ?? "default";
|
|
15815
15864
|
switch (assertionType) {
|
|
15816
15865
|
case "element_exists": {
|
|
15817
15866
|
if (!selector)
|
|
@@ -15876,7 +15925,6 @@ async function executeTool(page, screenshotter, toolName, toolInput, context) {
|
|
|
15876
15925
|
case "browser_intercept": {
|
|
15877
15926
|
const action = toolInput.action;
|
|
15878
15927
|
const pattern = toolInput.pattern;
|
|
15879
|
-
const interceptAction = toolInput.intercept_action;
|
|
15880
15928
|
const statusCode = toolInput.status_code;
|
|
15881
15929
|
const body = toolInput.body;
|
|
15882
15930
|
const sessionId = context.sessionId ?? "default";
|
|
@@ -15953,7 +16001,28 @@ ${JSON.stringify(har, null, 2)}` };
|
|
|
15953
16001
|
}
|
|
15954
16002
|
case "browser_a11y": {
|
|
15955
16003
|
const level = toolInput.level ?? "AA";
|
|
15956
|
-
const snapshot = await page.
|
|
16004
|
+
const snapshot = await page.evaluate(() => {
|
|
16005
|
+
function readRole(el) {
|
|
16006
|
+
return el.getAttribute("role") ?? el.tagName.toLowerCase();
|
|
16007
|
+
}
|
|
16008
|
+
function readName(el) {
|
|
16009
|
+
const labelledBy = el.getAttribute("aria-labelledby");
|
|
16010
|
+
if (labelledBy) {
|
|
16011
|
+
const labelledText = labelledBy.split(/\s+/).map((id) => document.getElementById(id)?.textContent?.trim()).filter(Boolean).join(" ");
|
|
16012
|
+
if (labelledText)
|
|
16013
|
+
return labelledText;
|
|
16014
|
+
}
|
|
16015
|
+
return el.getAttribute("aria-label") ?? el.getAttribute("alt") ?? el.textContent?.trim() ?? "";
|
|
16016
|
+
}
|
|
16017
|
+
function walk(el) {
|
|
16018
|
+
return {
|
|
16019
|
+
role: readRole(el),
|
|
16020
|
+
name: readName(el),
|
|
16021
|
+
children: Array.from(el.children).map((child) => walk(child))
|
|
16022
|
+
};
|
|
16023
|
+
}
|
|
16024
|
+
return document.body ? walk(document.body) : null;
|
|
16025
|
+
});
|
|
15957
16026
|
if (!snapshot)
|
|
15958
16027
|
return { result: "Error: could not capture accessibility tree" };
|
|
15959
16028
|
const issues = [];
|
|
@@ -15995,6 +16064,38 @@ ${filtered.join(`
|
|
|
15995
16064
|
return { result: `Error executing ${toolName}: ${message}` };
|
|
15996
16065
|
}
|
|
15997
16066
|
}
|
|
16067
|
+
function resolveStartUrl(baseUrl, targetPath) {
|
|
16068
|
+
try {
|
|
16069
|
+
return new URL(targetPath, baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`).toString();
|
|
16070
|
+
} catch {
|
|
16071
|
+
return `${baseUrl.replace(/\/+$/, "")}/${targetPath.replace(/^\/+/, "")}`;
|
|
16072
|
+
}
|
|
16073
|
+
}
|
|
16074
|
+
function buildScenarioUserMessage(scenario, baseUrl) {
|
|
16075
|
+
const userParts = [
|
|
16076
|
+
`**Scenario:** ${scenario.name}`,
|
|
16077
|
+
`**Description:** ${scenario.description}`
|
|
16078
|
+
];
|
|
16079
|
+
if (baseUrl) {
|
|
16080
|
+
const normalizedBaseUrl = baseUrl.replace(/\/+$/, "");
|
|
16081
|
+
userParts.push(`**Base URL:** ${normalizedBaseUrl}`);
|
|
16082
|
+
if (scenario.targetPath) {
|
|
16083
|
+
userParts.push(`**Start URL:** ${resolveStartUrl(normalizedBaseUrl, scenario.targetPath)}`);
|
|
16084
|
+
}
|
|
16085
|
+
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.");
|
|
16086
|
+
}
|
|
16087
|
+
if (scenario.targetPath) {
|
|
16088
|
+
userParts.push(`**Target Path:** ${scenario.targetPath}`);
|
|
16089
|
+
}
|
|
16090
|
+
if (scenario.steps.length > 0) {
|
|
16091
|
+
userParts.push("**Steps:**");
|
|
16092
|
+
for (let i = 0;i < scenario.steps.length; i++) {
|
|
16093
|
+
userParts.push(`${i + 1}. ${scenario.steps[i]}`);
|
|
16094
|
+
}
|
|
16095
|
+
}
|
|
16096
|
+
return userParts.join(`
|
|
16097
|
+
`);
|
|
16098
|
+
}
|
|
15998
16099
|
async function runAgentLoop(options) {
|
|
15999
16100
|
const {
|
|
16000
16101
|
client,
|
|
@@ -16004,6 +16105,7 @@ async function runAgentLoop(options) {
|
|
|
16004
16105
|
model,
|
|
16005
16106
|
runId,
|
|
16006
16107
|
sessionId,
|
|
16108
|
+
baseUrl,
|
|
16007
16109
|
maxTurns = 30,
|
|
16008
16110
|
onStep,
|
|
16009
16111
|
persona,
|
|
@@ -16051,21 +16153,7 @@ Instructions: ${persona.instructions}` : "",
|
|
|
16051
16153
|
"- Verify both positive and negative states"
|
|
16052
16154
|
].join(`
|
|
16053
16155
|
`) + personaSection;
|
|
16054
|
-
const
|
|
16055
|
-
`**Scenario:** ${scenario.name}`,
|
|
16056
|
-
`**Description:** ${scenario.description}`
|
|
16057
|
-
];
|
|
16058
|
-
if (scenario.targetPath) {
|
|
16059
|
-
userParts.push(`**Target Path:** ${scenario.targetPath}`);
|
|
16060
|
-
}
|
|
16061
|
-
if (scenario.steps.length > 0) {
|
|
16062
|
-
userParts.push("**Steps:**");
|
|
16063
|
-
for (let i = 0;i < scenario.steps.length; i++) {
|
|
16064
|
-
userParts.push(`${i + 1}. ${scenario.steps[i]}`);
|
|
16065
|
-
}
|
|
16066
|
-
}
|
|
16067
|
-
const userMessage = userParts.join(`
|
|
16068
|
-
`);
|
|
16156
|
+
const userMessage = buildScenarioUserMessage(scenario, baseUrl);
|
|
16069
16157
|
const screenshots = [];
|
|
16070
16158
|
let tokensUsed = 0;
|
|
16071
16159
|
let stepNumber = 0;
|
|
@@ -16128,7 +16216,7 @@ Instructions: ${persona.instructions}` : "",
|
|
|
16128
16216
|
if (onStep) {
|
|
16129
16217
|
onStep({ type: "tool_call", toolName: toolBlock.name, toolInput, stepNumber });
|
|
16130
16218
|
}
|
|
16131
|
-
const execResult = await executeTool(page, screenshotter, toolBlock.name, toolInput, { runId, scenarioSlug, stepNumber, sessionId, a11y });
|
|
16219
|
+
const execResult = await executeTool(page, screenshotter, toolBlock.name, toolInput, { runId, scenarioSlug, stepNumber, sessionId: sessionId ?? runId, a11y });
|
|
16132
16220
|
if (onStep) {
|
|
16133
16221
|
onStep({ type: "tool_result", toolName: toolBlock.name, toolResult: execResult.result, stepNumber });
|
|
16134
16222
|
}
|
|
@@ -46800,11 +46888,11 @@ var init_scan_issues = __esm(() => {
|
|
|
46800
46888
|
|
|
46801
46889
|
// src/server/index.ts
|
|
46802
46890
|
import { existsSync as existsSync10 } from "fs";
|
|
46803
|
-
import { join as
|
|
46891
|
+
import { join as join14 } from "path";
|
|
46804
46892
|
// package.json
|
|
46805
46893
|
var package_default = {
|
|
46806
46894
|
name: "@hasna/testers",
|
|
46807
|
-
version: "0.0.
|
|
46895
|
+
version: "0.0.35",
|
|
46808
46896
|
description: "AI-powered QA testing CLI \u2014 spawns cheap AI agents to test web apps with headless browsers",
|
|
46809
46897
|
type: "module",
|
|
46810
46898
|
main: "dist/index.js",
|
|
@@ -46828,10 +46916,10 @@ var package_default = {
|
|
|
46828
46916
|
],
|
|
46829
46917
|
scripts: {
|
|
46830
46918
|
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",
|
|
46831
|
-
"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",
|
|
46832
|
-
"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",
|
|
46833
|
-
"build:server": "bun build src/server/index.ts --outdir dist/server --target bun --external @anthropic-ai/sdk --external playwright --external @hasna/browser",
|
|
46834
|
-
"build:lib": "bun build src/index.ts --outdir dist --target bun --external playwright --external @anthropic-ai/sdk --external @modelcontextprotocol/sdk --external @hasna/browser",
|
|
46919
|
+
"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",
|
|
46920
|
+
"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",
|
|
46921
|
+
"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",
|
|
46922
|
+
"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",
|
|
46835
46923
|
"build:types": "NODE_OPTIONS='--max-old-space-size=8192' tsc --emitDeclarationOnly --outDir dist --skipLibCheck || true",
|
|
46836
46924
|
"build:dashboard": "cd dashboard && bun run build",
|
|
46837
46925
|
"build:ext": "cd extension && bun run build",
|
|
@@ -46845,10 +46933,11 @@ var package_default = {
|
|
|
46845
46933
|
},
|
|
46846
46934
|
dependencies: {
|
|
46847
46935
|
"@anthropic-ai/sdk": "^0.52.0",
|
|
46848
|
-
"@hasna/browser": "^0.4.
|
|
46936
|
+
"@hasna/browser": "^0.4.12",
|
|
46849
46937
|
"@hasna/cloud": "^0.1.24",
|
|
46850
46938
|
"@hasna/contacts": "^0.6.8",
|
|
46851
46939
|
"@hasna/projects": "^0.1.42",
|
|
46940
|
+
"@hasna/sandboxes": "^0.1.27",
|
|
46852
46941
|
"@modelcontextprotocol/sdk": "^1.12.1",
|
|
46853
46942
|
ai: "^6.0.175",
|
|
46854
46943
|
chalk: "^5.4.1",
|
|
@@ -49002,12 +49091,345 @@ async function notifyRunToConversations(run, results, options) {
|
|
|
49002
49091
|
} catch {}
|
|
49003
49092
|
}
|
|
49004
49093
|
|
|
49094
|
+
// src/lib/a11y-audit.ts
|
|
49095
|
+
async function runA11yAudit(page, options = {}) {
|
|
49096
|
+
const { level = "AA", rules, exclude = [] } = options;
|
|
49097
|
+
await page.addScriptTag({ url: "https://cdnjs.cloudflare.com/ajax/libs/axe-core/4.9.1/axe.min.js" });
|
|
49098
|
+
const config = {
|
|
49099
|
+
runOnly: {
|
|
49100
|
+
type: level === "AAA" ? "standard" : "tag",
|
|
49101
|
+
values: level === "AAA" ? undefined : [level, "best-practice"]
|
|
49102
|
+
}
|
|
49103
|
+
};
|
|
49104
|
+
if (rules && rules.length > 0) {
|
|
49105
|
+
config.rules = Object.fromEntries(rules.map((r) => [r, { enabled: true }]));
|
|
49106
|
+
}
|
|
49107
|
+
if (exclude.length > 0) {
|
|
49108
|
+
config.exclude = exclude;
|
|
49109
|
+
}
|
|
49110
|
+
const result = await page.evaluate(async (auditConfig) => {
|
|
49111
|
+
const axeResult = await window.axe.run(auditConfig);
|
|
49112
|
+
return axeResult;
|
|
49113
|
+
}, config);
|
|
49114
|
+
const violations = (result.violations ?? []).map((v) => ({
|
|
49115
|
+
id: v.id,
|
|
49116
|
+
impact: v.impact,
|
|
49117
|
+
description: v.description,
|
|
49118
|
+
help: v.help,
|
|
49119
|
+
helpUrl: v.helpUrl,
|
|
49120
|
+
nodes: (v.nodes ?? []).map((n) => ({
|
|
49121
|
+
html: n.html,
|
|
49122
|
+
target: n.target,
|
|
49123
|
+
failureSummary: n.failureSummary
|
|
49124
|
+
}))
|
|
49125
|
+
}));
|
|
49126
|
+
const passes = (result.passes ?? []).map((p) => ({
|
|
49127
|
+
id: p.id,
|
|
49128
|
+
description: p.description
|
|
49129
|
+
}));
|
|
49130
|
+
const incomplete = (result.incomplete ?? []).map((i) => ({
|
|
49131
|
+
id: i.id,
|
|
49132
|
+
description: i.description,
|
|
49133
|
+
impact: i.impact
|
|
49134
|
+
}));
|
|
49135
|
+
const criticalCount = violations.filter((v) => v.impact === "critical").length;
|
|
49136
|
+
const seriousCount = violations.filter((v) => v.impact === "serious").length;
|
|
49137
|
+
const moderateCount = violations.filter((v) => v.impact === "moderate").length;
|
|
49138
|
+
const minorCount = violations.filter((v) => v.impact === "minor").length;
|
|
49139
|
+
return {
|
|
49140
|
+
violations,
|
|
49141
|
+
passes,
|
|
49142
|
+
incomplete,
|
|
49143
|
+
url: page.url(),
|
|
49144
|
+
timestamp: new Date().toISOString(),
|
|
49145
|
+
totalViolations: violations.length,
|
|
49146
|
+
criticalCount,
|
|
49147
|
+
seriousCount,
|
|
49148
|
+
moderateCount,
|
|
49149
|
+
minorCount
|
|
49150
|
+
};
|
|
49151
|
+
}
|
|
49152
|
+
|
|
49153
|
+
// src/lib/assertions.ts
|
|
49154
|
+
async function evaluateAssertions(page, assertions, context = {}) {
|
|
49155
|
+
const results = [];
|
|
49156
|
+
for (const assertion of assertions) {
|
|
49157
|
+
try {
|
|
49158
|
+
const result = await evaluateOne(page, assertion, context);
|
|
49159
|
+
results.push(result);
|
|
49160
|
+
} catch (err) {
|
|
49161
|
+
results.push({
|
|
49162
|
+
assertion,
|
|
49163
|
+
passed: false,
|
|
49164
|
+
actual: "",
|
|
49165
|
+
error: err instanceof Error ? err.message : String(err)
|
|
49166
|
+
});
|
|
49167
|
+
}
|
|
49168
|
+
}
|
|
49169
|
+
return results;
|
|
49170
|
+
}
|
|
49171
|
+
async function evaluateOne(page, assertion, context) {
|
|
49172
|
+
switch (assertion.type) {
|
|
49173
|
+
case "visible": {
|
|
49174
|
+
const visible = await page.locator(assertion.selector).isVisible();
|
|
49175
|
+
return {
|
|
49176
|
+
assertion,
|
|
49177
|
+
passed: visible,
|
|
49178
|
+
actual: String(visible)
|
|
49179
|
+
};
|
|
49180
|
+
}
|
|
49181
|
+
case "not_visible": {
|
|
49182
|
+
const visible = await page.locator(assertion.selector).isVisible();
|
|
49183
|
+
return {
|
|
49184
|
+
assertion,
|
|
49185
|
+
passed: !visible,
|
|
49186
|
+
actual: String(visible)
|
|
49187
|
+
};
|
|
49188
|
+
}
|
|
49189
|
+
case "text_contains": {
|
|
49190
|
+
const text = await page.locator(assertion.selector).textContent() ?? "";
|
|
49191
|
+
const expected = String(assertion.expected ?? "");
|
|
49192
|
+
return {
|
|
49193
|
+
assertion,
|
|
49194
|
+
passed: text.includes(expected),
|
|
49195
|
+
actual: text
|
|
49196
|
+
};
|
|
49197
|
+
}
|
|
49198
|
+
case "text_equals": {
|
|
49199
|
+
const text = await page.locator(assertion.selector).textContent() ?? "";
|
|
49200
|
+
const expected = String(assertion.expected ?? "");
|
|
49201
|
+
return {
|
|
49202
|
+
assertion,
|
|
49203
|
+
passed: text.trim() === expected.trim(),
|
|
49204
|
+
actual: text
|
|
49205
|
+
};
|
|
49206
|
+
}
|
|
49207
|
+
case "element_count": {
|
|
49208
|
+
const count = await page.locator(assertion.selector).count();
|
|
49209
|
+
const expected = Number(assertion.expected ?? 0);
|
|
49210
|
+
return {
|
|
49211
|
+
assertion,
|
|
49212
|
+
passed: count === expected,
|
|
49213
|
+
actual: String(count)
|
|
49214
|
+
};
|
|
49215
|
+
}
|
|
49216
|
+
case "no_console_errors": {
|
|
49217
|
+
if (context.consoleErrors !== undefined) {
|
|
49218
|
+
const errors2 = context.consoleErrors.filter(Boolean);
|
|
49219
|
+
return {
|
|
49220
|
+
assertion,
|
|
49221
|
+
passed: errors2.length === 0,
|
|
49222
|
+
actual: errors2.length === 0 ? "No console errors captured" : errors2.slice(0, 3).join(" | ")
|
|
49223
|
+
};
|
|
49224
|
+
}
|
|
49225
|
+
const errorElements = await page.locator('[role="alert"], .error, .error-message, [data-testid="error"]').count();
|
|
49226
|
+
return {
|
|
49227
|
+
assertion,
|
|
49228
|
+
passed: errorElements === 0,
|
|
49229
|
+
actual: `${errorElements} error element(s) found`
|
|
49230
|
+
};
|
|
49231
|
+
}
|
|
49232
|
+
case "no_a11y_violations": {
|
|
49233
|
+
try {
|
|
49234
|
+
const auditResult = await runA11yAudit(page);
|
|
49235
|
+
const hasIssues = auditResult.violations.length > 0;
|
|
49236
|
+
return {
|
|
49237
|
+
assertion,
|
|
49238
|
+
passed: !hasIssues,
|
|
49239
|
+
actual: hasIssues ? `${auditResult.totalViolations} violation(s): ${auditResult.violations.map((v) => v.id).join(", ")}` : "No accessibility violations found"
|
|
49240
|
+
};
|
|
49241
|
+
} catch (err) {
|
|
49242
|
+
return {
|
|
49243
|
+
assertion,
|
|
49244
|
+
passed: false,
|
|
49245
|
+
actual: "",
|
|
49246
|
+
error: err instanceof Error ? err.message : String(err)
|
|
49247
|
+
};
|
|
49248
|
+
}
|
|
49249
|
+
}
|
|
49250
|
+
case "url_contains": {
|
|
49251
|
+
const url = page.url();
|
|
49252
|
+
const expected = String(assertion.expected ?? "");
|
|
49253
|
+
return {
|
|
49254
|
+
assertion,
|
|
49255
|
+
passed: url.includes(expected),
|
|
49256
|
+
actual: url
|
|
49257
|
+
};
|
|
49258
|
+
}
|
|
49259
|
+
case "title_contains": {
|
|
49260
|
+
const title = await page.title();
|
|
49261
|
+
const expected = String(assertion.expected ?? "");
|
|
49262
|
+
return {
|
|
49263
|
+
assertion,
|
|
49264
|
+
passed: title.includes(expected),
|
|
49265
|
+
actual: title
|
|
49266
|
+
};
|
|
49267
|
+
}
|
|
49268
|
+
case "cookie_exists": {
|
|
49269
|
+
const cookieName = assertion.expected;
|
|
49270
|
+
const cookies = await page.context().cookies();
|
|
49271
|
+
const found = cookies.some((c) => c.name === cookieName);
|
|
49272
|
+
return {
|
|
49273
|
+
assertion,
|
|
49274
|
+
passed: found,
|
|
49275
|
+
actual: found ? `Cookie "${cookieName}" exists` : `Cookie "${cookieName}" not found`
|
|
49276
|
+
};
|
|
49277
|
+
}
|
|
49278
|
+
case "cookie_not_exists": {
|
|
49279
|
+
const cookieName = assertion.expected;
|
|
49280
|
+
const cookies = await page.context().cookies();
|
|
49281
|
+
const found = cookies.some((c) => c.name === cookieName);
|
|
49282
|
+
return {
|
|
49283
|
+
assertion,
|
|
49284
|
+
passed: !found,
|
|
49285
|
+
actual: found ? `Cookie "${cookieName}" found (unexpected)` : `Cookie "${cookieName}" does not exist`
|
|
49286
|
+
};
|
|
49287
|
+
}
|
|
49288
|
+
case "cookie_value": {
|
|
49289
|
+
const [cookieName, expectedValue] = assertion.expected.split("=", 2);
|
|
49290
|
+
const cookies = await page.context().cookies();
|
|
49291
|
+
const cookie = cookies.find((c) => c.name === cookieName);
|
|
49292
|
+
const actualValue = cookie?.value ?? "";
|
|
49293
|
+
return {
|
|
49294
|
+
assertion,
|
|
49295
|
+
passed: actualValue === expectedValue,
|
|
49296
|
+
actual: cookie ? `${cookieName}=${actualValue}` : `Cookie "${cookieName}" not found`
|
|
49297
|
+
};
|
|
49298
|
+
}
|
|
49299
|
+
case "local_storage_exists": {
|
|
49300
|
+
const key = assertion.expected;
|
|
49301
|
+
const value = await page.evaluate((k) => localStorage.getItem(k), key);
|
|
49302
|
+
return {
|
|
49303
|
+
assertion,
|
|
49304
|
+
passed: value !== null,
|
|
49305
|
+
actual: value !== null ? `Key "${key}" exists with value "${value}"` : `Key "${key}" not found in localStorage`
|
|
49306
|
+
};
|
|
49307
|
+
}
|
|
49308
|
+
case "local_storage_not_exists": {
|
|
49309
|
+
const key = assertion.expected;
|
|
49310
|
+
const value = await page.evaluate((k) => localStorage.getItem(k), key);
|
|
49311
|
+
return {
|
|
49312
|
+
assertion,
|
|
49313
|
+
passed: value === null,
|
|
49314
|
+
actual: value !== null ? `Key "${key}" exists (unexpected)` : `Key "${key}" does not exist in localStorage`
|
|
49315
|
+
};
|
|
49316
|
+
}
|
|
49317
|
+
case "local_storage_value": {
|
|
49318
|
+
const [lsKey, expectedValue] = assertion.expected.split("=", 2);
|
|
49319
|
+
const value = await page.evaluate((k) => localStorage.getItem(k), lsKey ?? "");
|
|
49320
|
+
return {
|
|
49321
|
+
assertion,
|
|
49322
|
+
passed: value === expectedValue,
|
|
49323
|
+
actual: value !== null ? `${lsKey}=${value}` : `Key "${lsKey}" not found in localStorage`
|
|
49324
|
+
};
|
|
49325
|
+
}
|
|
49326
|
+
case "session_storage_value": {
|
|
49327
|
+
const [ssKey, expectedValue] = assertion.expected.split("=", 2);
|
|
49328
|
+
const value = await page.evaluate((k) => sessionStorage.getItem(k), ssKey ?? "");
|
|
49329
|
+
return {
|
|
49330
|
+
assertion,
|
|
49331
|
+
passed: value === expectedValue,
|
|
49332
|
+
actual: value !== null ? `${ssKey}=${value}` : `Key "${ssKey}" not found in sessionStorage`
|
|
49333
|
+
};
|
|
49334
|
+
}
|
|
49335
|
+
case "session_storage_not_exists": {
|
|
49336
|
+
const key = assertion.expected;
|
|
49337
|
+
const value = await page.evaluate((k) => sessionStorage.getItem(k), key);
|
|
49338
|
+
return {
|
|
49339
|
+
assertion,
|
|
49340
|
+
passed: value === null,
|
|
49341
|
+
actual: value !== null ? `Key "${key}" exists (unexpected)` : `Key "${key}" does not exist in sessionStorage`
|
|
49342
|
+
};
|
|
49343
|
+
}
|
|
49344
|
+
default: {
|
|
49345
|
+
return {
|
|
49346
|
+
assertion,
|
|
49347
|
+
passed: false,
|
|
49348
|
+
actual: "",
|
|
49349
|
+
error: `Unknown assertion type: ${assertion.type}`
|
|
49350
|
+
};
|
|
49351
|
+
}
|
|
49352
|
+
}
|
|
49353
|
+
}
|
|
49354
|
+
function allAssertionsPassed(results) {
|
|
49355
|
+
return results.every((r) => r.passed);
|
|
49356
|
+
}
|
|
49357
|
+
function formatAssertionResults(results) {
|
|
49358
|
+
if (results.length === 0)
|
|
49359
|
+
return "No assertions.";
|
|
49360
|
+
const lines = [];
|
|
49361
|
+
for (const r of results) {
|
|
49362
|
+
const icon = r.passed ? "PASS" : "FAIL";
|
|
49363
|
+
const desc = r.assertion.description || `${r.assertion.type}${r.assertion.selector ? ` ${r.assertion.selector}` : ""}`;
|
|
49364
|
+
let line = ` [${icon}] ${desc}`;
|
|
49365
|
+
if (!r.passed) {
|
|
49366
|
+
line += ` (actual: ${r.actual})`;
|
|
49367
|
+
if (r.error)
|
|
49368
|
+
line += ` \u2014 ${r.error}`;
|
|
49369
|
+
}
|
|
49370
|
+
lines.push(line);
|
|
49371
|
+
}
|
|
49372
|
+
const passed = results.filter((r) => r.passed).length;
|
|
49373
|
+
lines.push(`
|
|
49374
|
+
${passed}/${results.length} assertions passed.`);
|
|
49375
|
+
return lines.join(`
|
|
49376
|
+
`);
|
|
49377
|
+
}
|
|
49378
|
+
|
|
49005
49379
|
// src/lib/runner.ts
|
|
49006
49380
|
var eventHandler = null;
|
|
49007
49381
|
function emit(event) {
|
|
49008
49382
|
if (eventHandler)
|
|
49009
49383
|
eventHandler(event);
|
|
49010
49384
|
}
|
|
49385
|
+
function assertionDescription(result) {
|
|
49386
|
+
return result.assertion.description || `${result.assertion.type}${result.assertion.selector ? ` ${result.assertion.selector}` : ""}`;
|
|
49387
|
+
}
|
|
49388
|
+
function summarizeAssertionResult(result) {
|
|
49389
|
+
const description = assertionDescription(result);
|
|
49390
|
+
if (result.passed)
|
|
49391
|
+
return description;
|
|
49392
|
+
const suffix = result.error ? `; ${result.error}` : "";
|
|
49393
|
+
return `${description} (actual: ${result.actual}${suffix})`;
|
|
49394
|
+
}
|
|
49395
|
+
async function applyStructuredAssertionsToResult(input) {
|
|
49396
|
+
const assertions = input.scenario.assertions ?? [];
|
|
49397
|
+
if (assertions.length === 0) {
|
|
49398
|
+
return {
|
|
49399
|
+
status: input.status,
|
|
49400
|
+
reasoning: input.reasoning,
|
|
49401
|
+
assertionsPassed: [],
|
|
49402
|
+
assertionsFailed: [],
|
|
49403
|
+
assertionResults: []
|
|
49404
|
+
};
|
|
49405
|
+
}
|
|
49406
|
+
const results = await evaluateAssertions(input.page, assertions, {
|
|
49407
|
+
consoleErrors: input.consoleErrors
|
|
49408
|
+
});
|
|
49409
|
+
const assertionsPassed = results.filter((r) => r.passed).map(summarizeAssertionResult);
|
|
49410
|
+
const assertionsFailed = results.filter((r) => !r.passed).map(summarizeAssertionResult);
|
|
49411
|
+
const assertionResults = results.map((result) => ({
|
|
49412
|
+
type: result.assertion.type,
|
|
49413
|
+
description: assertionDescription(result),
|
|
49414
|
+
passed: result.passed,
|
|
49415
|
+
actual: result.actual,
|
|
49416
|
+
...result.error ? { error: result.error } : {}
|
|
49417
|
+
}));
|
|
49418
|
+
const assertionsOk = allAssertionsPassed(results);
|
|
49419
|
+
const status = assertionsOk || input.status !== "passed" ? input.status : "failed";
|
|
49420
|
+
const assertionHeading = assertionsOk ? "Structured assertions passed:" : "Structured assertions failed:";
|
|
49421
|
+
const reasoningParts = [input.reasoning, `${assertionHeading}
|
|
49422
|
+
${formatAssertionResults(results)}`].map((part) => part.trim()).filter(Boolean);
|
|
49423
|
+
return {
|
|
49424
|
+
status,
|
|
49425
|
+
reasoning: reasoningParts.join(`
|
|
49426
|
+
|
|
49427
|
+
`),
|
|
49428
|
+
assertionsPassed,
|
|
49429
|
+
assertionsFailed,
|
|
49430
|
+
assertionResults
|
|
49431
|
+
};
|
|
49432
|
+
}
|
|
49011
49433
|
function withTimeout(promise, ms, label) {
|
|
49012
49434
|
return new Promise((resolve, reject) => {
|
|
49013
49435
|
const warningAt = Math.floor(ms * 0.8);
|
|
@@ -49178,6 +49600,7 @@ async function runSingleScenario(scenario, runId, options) {
|
|
|
49178
49600
|
model,
|
|
49179
49601
|
runId,
|
|
49180
49602
|
sessionId: result.id,
|
|
49603
|
+
baseUrl: options.url,
|
|
49181
49604
|
maxTurns: effectiveOptions.minimal ? 10 : 30,
|
|
49182
49605
|
a11y: effectiveOptions.a11y,
|
|
49183
49606
|
persona: persona ? {
|
|
@@ -49260,27 +49683,46 @@ async function runSingleScenario(scenario, runId, options) {
|
|
|
49260
49683
|
closeSession(result.id);
|
|
49261
49684
|
const lightpandaNote = options.engine === "lightpanda" ? " (Running with Lightpanda \u2014 no screenshots)" : options.engine === "bun" ? " (Running with Bun.WebView \u2014 native, ~11x faster)" : "";
|
|
49262
49685
|
const networkMeta = networkErrors.length > 0 ? { networkErrors: networkErrors.slice(0, 20) } : {};
|
|
49263
|
-
|
|
49686
|
+
const baseReasoning = agentResult.reasoning ? agentResult.reasoning + lightpandaNote : lightpandaNote || "";
|
|
49687
|
+
const assertionOutcome = await applyStructuredAssertionsToResult({
|
|
49688
|
+
page,
|
|
49689
|
+
scenario,
|
|
49690
|
+
consoleErrors,
|
|
49264
49691
|
status: agentResult.status,
|
|
49265
|
-
reasoning:
|
|
49692
|
+
reasoning: baseReasoning
|
|
49693
|
+
});
|
|
49694
|
+
const structuredAssertionMeta = assertionOutcome.assertionResults.length > 0 ? {
|
|
49695
|
+
structuredAssertions: {
|
|
49696
|
+
passed: assertionOutcome.assertionsPassed,
|
|
49697
|
+
failed: assertionOutcome.assertionsFailed,
|
|
49698
|
+
results: assertionOutcome.assertionResults
|
|
49699
|
+
}
|
|
49700
|
+
} : {};
|
|
49701
|
+
let updatedResult = updateResult(result.id, {
|
|
49702
|
+
status: assertionOutcome.status,
|
|
49703
|
+
reasoning: assertionOutcome.reasoning || undefined,
|
|
49266
49704
|
stepsCompleted: agentResult.stepsCompleted,
|
|
49267
49705
|
durationMs: Date.now() - new Date(result.createdAt).getTime(),
|
|
49268
49706
|
tokensUsed: agentResult.tokensUsed,
|
|
49269
49707
|
costCents: estimateCost(model, agentResult.tokensUsed),
|
|
49270
|
-
metadata: {
|
|
49708
|
+
metadata: {
|
|
49709
|
+
consoleLogs,
|
|
49710
|
+
...networkErrors.length > 0 ? networkMeta : {},
|
|
49711
|
+
...structuredAssertionMeta
|
|
49712
|
+
}
|
|
49271
49713
|
});
|
|
49272
|
-
if (
|
|
49273
|
-
const failureAnalysis = analyzeFailure(null,
|
|
49714
|
+
if (assertionOutcome.status === "failed" || assertionOutcome.status === "error") {
|
|
49715
|
+
const failureAnalysis = analyzeFailure(null, assertionOutcome.reasoning ?? null);
|
|
49274
49716
|
if (failureAnalysis) {
|
|
49275
49717
|
updatedResult = updateResult(result.id, { failureAnalysis });
|
|
49276
49718
|
}
|
|
49277
49719
|
}
|
|
49278
|
-
if (
|
|
49720
|
+
if (assertionOutcome.status === "passed") {
|
|
49279
49721
|
try {
|
|
49280
49722
|
updateScenarioPassedCache(scenario.id, options.url);
|
|
49281
49723
|
} catch {}
|
|
49282
49724
|
}
|
|
49283
|
-
const eventType =
|
|
49725
|
+
const eventType = assertionOutcome.status === "passed" ? "scenario:pass" : "scenario:fail";
|
|
49284
49726
|
emit({ type: eventType, scenarioId: scenario.id, scenarioName: scenario.name, resultId: result.id, runId });
|
|
49285
49727
|
return updatedResult;
|
|
49286
49728
|
} catch (error) {
|
|
@@ -49305,7 +49747,8 @@ async function runSingleScenario(scenario, runId, options) {
|
|
|
49305
49747
|
} finally {
|
|
49306
49748
|
if (harPath) {
|
|
49307
49749
|
try {
|
|
49308
|
-
|
|
49750
|
+
const existing = getResult(result.id);
|
|
49751
|
+
updateResult(result.id, { metadata: { ...existing?.metadata ?? {}, harPath } });
|
|
49309
49752
|
} catch {}
|
|
49310
49753
|
}
|
|
49311
49754
|
if (browser) {
|
|
@@ -49477,22 +49920,31 @@ async function runBatch(scenarios, options) {
|
|
|
49477
49920
|
}
|
|
49478
49921
|
return { run: finalRun, results };
|
|
49479
49922
|
}
|
|
49480
|
-
|
|
49481
|
-
|
|
49923
|
+
function findScenarioInList(scenarios, id) {
|
|
49924
|
+
return scenarios.find((scenario) => scenario.id === id || scenario.shortId === id || scenario.id.startsWith(id)) ?? null;
|
|
49925
|
+
}
|
|
49926
|
+
function resolveScenariosForRun(options) {
|
|
49482
49927
|
if (options.scenarioIds && options.scenarioIds.length > 0) {
|
|
49483
|
-
const
|
|
49484
|
-
|
|
49485
|
-
|
|
49486
|
-
|
|
49487
|
-
|
|
49928
|
+
const scoped = listScenarios({ projectId: options.projectId });
|
|
49929
|
+
const resolved = [];
|
|
49930
|
+
const seen = new Set;
|
|
49931
|
+
for (const id of options.scenarioIds) {
|
|
49932
|
+
const scenario = findScenarioInList(scoped, id) ?? getScenario(id);
|
|
49933
|
+
if (scenario && !seen.has(scenario.id)) {
|
|
49934
|
+
resolved.push(scenario);
|
|
49935
|
+
seen.add(scenario.id);
|
|
49936
|
+
}
|
|
49488
49937
|
}
|
|
49489
|
-
|
|
49490
|
-
scenarios = listScenarios({
|
|
49491
|
-
projectId: options.projectId,
|
|
49492
|
-
tags: options.tags,
|
|
49493
|
-
priority: options.priority
|
|
49494
|
-
});
|
|
49938
|
+
return resolved;
|
|
49495
49939
|
}
|
|
49940
|
+
return listScenarios({
|
|
49941
|
+
projectId: options.projectId,
|
|
49942
|
+
tags: options.tags,
|
|
49943
|
+
priority: options.priority
|
|
49944
|
+
});
|
|
49945
|
+
}
|
|
49946
|
+
async function runByFilter(options) {
|
|
49947
|
+
const scenarios = resolveScenariosForRun(options);
|
|
49496
49948
|
if (scenarios.length === 0) {
|
|
49497
49949
|
const config = loadConfig();
|
|
49498
49950
|
const model = resolveModel(options.model ?? config.defaultModel);
|
|
@@ -50656,18 +51108,7 @@ function normalizeFilter(input) {
|
|
|
50656
51108
|
};
|
|
50657
51109
|
}
|
|
50658
51110
|
function normalizeExecution(input) {
|
|
50659
|
-
|
|
50660
|
-
if (target === "connector:e2b") {
|
|
50661
|
-
return {
|
|
50662
|
-
target,
|
|
50663
|
-
connector: input?.connector ?? "e2b",
|
|
50664
|
-
operation: input?.operation ?? "run",
|
|
50665
|
-
sandboxTemplate: input?.sandboxTemplate,
|
|
50666
|
-
timeoutMs: input?.timeoutMs,
|
|
50667
|
-
env: input?.env
|
|
50668
|
-
};
|
|
50669
|
-
}
|
|
50670
|
-
return { ...DEFAULT_EXECUTION, timeoutMs: input?.timeoutMs };
|
|
51111
|
+
return input ? workflowExecutionFromValue(input) : DEFAULT_EXECUTION;
|
|
50671
51112
|
}
|
|
50672
51113
|
function createTestingWorkflow(input) {
|
|
50673
51114
|
const db2 = getDatabase();
|
|
@@ -50766,6 +51207,10 @@ function db2() {
|
|
|
50766
51207
|
}
|
|
50767
51208
|
|
|
50768
51209
|
// src/lib/workflow-runner.ts
|
|
51210
|
+
init_database();
|
|
51211
|
+
import { mkdtempSync, rmSync, writeFileSync as writeFileSync3 } from "fs";
|
|
51212
|
+
import { tmpdir } from "os";
|
|
51213
|
+
import { join as join13 } from "path";
|
|
50769
51214
|
function buildWorkflowRunPlan(workflow, options) {
|
|
50770
51215
|
const runOptions = {
|
|
50771
51216
|
url: options.url,
|
|
@@ -50782,10 +51227,10 @@ function buildWorkflowRunPlan(workflow, options) {
|
|
|
50782
51227
|
return {
|
|
50783
51228
|
workflow,
|
|
50784
51229
|
runOptions,
|
|
50785
|
-
|
|
51230
|
+
sandbox: workflow.execution.target === "sandbox" ? buildSandboxPlan(workflow, workflow.execution, runOptions) : null
|
|
50786
51231
|
};
|
|
50787
51232
|
}
|
|
50788
|
-
async function runTestingWorkflow(workflowId, options) {
|
|
51233
|
+
async function runTestingWorkflow(workflowId, options, dependencies = {}) {
|
|
50789
51234
|
const workflow = getTestingWorkflow(workflowId);
|
|
50790
51235
|
if (!workflow)
|
|
50791
51236
|
throw new Error(`Testing workflow not found: ${workflowId}`);
|
|
@@ -50795,13 +51240,25 @@ async function runTestingWorkflow(workflowId, options) {
|
|
|
50795
51240
|
const plan = buildWorkflowRunPlan(workflow, options);
|
|
50796
51241
|
if (options.dryRun)
|
|
50797
51242
|
return { run: null, results: [], plan };
|
|
50798
|
-
if (workflow.execution.target === "
|
|
50799
|
-
const
|
|
50800
|
-
return { run: null, results: [], plan,
|
|
51243
|
+
if (workflow.execution.target === "sandbox") {
|
|
51244
|
+
const sandboxResult = await runViaSandbox(plan, dependencies);
|
|
51245
|
+
return { run: null, results: [], plan, sandboxResult };
|
|
50801
51246
|
}
|
|
50802
|
-
const
|
|
51247
|
+
const runLocal = dependencies.runByFilter ?? runByFilter;
|
|
51248
|
+
const { run, results } = await runLocal(plan.runOptions);
|
|
50803
51249
|
return { run, results, plan };
|
|
50804
51250
|
}
|
|
51251
|
+
function createWorkflowDatabaseBundle(workflow, plan) {
|
|
51252
|
+
if (!plan.sandbox)
|
|
51253
|
+
throw new Error(`Workflow is not configured for sandbox execution: ${workflow.name}`);
|
|
51254
|
+
const localDir = mkdtempSync(join13(tmpdir(), `testers-workflow-${workflow.id.slice(0, 8)}-`));
|
|
51255
|
+
writeFileSync3(join13(localDir, "testers.db"), getDatabase().serialize());
|
|
51256
|
+
return {
|
|
51257
|
+
localDir,
|
|
51258
|
+
remoteDir: plan.sandbox.stateRemoteDir,
|
|
51259
|
+
cleanup: () => rmSync(localDir, { recursive: true, force: true })
|
|
51260
|
+
};
|
|
51261
|
+
}
|
|
50805
51262
|
function validatePersonaIds(workflow) {
|
|
50806
51263
|
for (const personaId of workflow.personaIds) {
|
|
50807
51264
|
if (!getPersona(personaId)) {
|
|
@@ -50809,46 +51266,109 @@ function validatePersonaIds(workflow) {
|
|
|
50809
51266
|
}
|
|
50810
51267
|
}
|
|
50811
51268
|
}
|
|
50812
|
-
function
|
|
50813
|
-
const
|
|
50814
|
-
const
|
|
50815
|
-
|
|
50816
|
-
|
|
50817
|
-
|
|
51269
|
+
function buildSandboxPlan(workflow, execution, runOptions) {
|
|
51270
|
+
const remoteDir = execution.sandboxRemoteDir ?? `/tmp/testers-workflow-${workflow.id.slice(0, 8)}`;
|
|
51271
|
+
const stateRemoteDir = `${remoteDir.replace(/\/+$/, "")}/.testers-state`;
|
|
51272
|
+
return {
|
|
51273
|
+
provider: execution.provider,
|
|
51274
|
+
image: execution.sandboxImage,
|
|
51275
|
+
name: `testers-${workflow.id.slice(0, 8)}`,
|
|
51276
|
+
remoteDir,
|
|
51277
|
+
stateRemoteDir,
|
|
51278
|
+
cleanup: execution.sandboxCleanup ?? "delete",
|
|
50818
51279
|
timeoutMs: execution.timeoutMs,
|
|
50819
|
-
env: execution.env
|
|
50820
|
-
command:
|
|
50821
|
-
|
|
50822
|
-
|
|
50823
|
-
|
|
50824
|
-
|
|
50825
|
-
|
|
50826
|
-
|
|
50827
|
-
|
|
50828
|
-
...runOptions.projectId ? ["--project", runOptions.projectId] : [],
|
|
50829
|
-
...runOptions.model ? ["--model", runOptions.model] : [],
|
|
50830
|
-
"--json"
|
|
50831
|
-
]
|
|
50832
|
-
});
|
|
50833
|
-
return ["connectors", "run", connector, operation, payload];
|
|
51280
|
+
env: execution.env,
|
|
51281
|
+
command: buildSandboxCommand({
|
|
51282
|
+
runOptions,
|
|
51283
|
+
remoteDir,
|
|
51284
|
+
dbPath: `${stateRemoteDir}/testers.db`,
|
|
51285
|
+
setupCommand: execution.setupCommand,
|
|
51286
|
+
packageSpec: execution.packageSpec ?? "@hasna/testers"
|
|
51287
|
+
})
|
|
51288
|
+
};
|
|
50834
51289
|
}
|
|
50835
|
-
|
|
50836
|
-
|
|
50837
|
-
|
|
50838
|
-
|
|
50839
|
-
|
|
50840
|
-
|
|
50841
|
-
|
|
50842
|
-
|
|
50843
|
-
|
|
50844
|
-
|
|
50845
|
-
|
|
50846
|
-
|
|
50847
|
-
|
|
50848
|
-
|
|
50849
|
-
|
|
51290
|
+
function buildSandboxCommand(input) {
|
|
51291
|
+
const args = [
|
|
51292
|
+
"bunx",
|
|
51293
|
+
input.packageSpec,
|
|
51294
|
+
"run",
|
|
51295
|
+
input.runOptions.url,
|
|
51296
|
+
...input.runOptions.scenarioIds?.length ? ["--scenario", input.runOptions.scenarioIds.join(",")] : [],
|
|
51297
|
+
...input.runOptions.tags?.length ? input.runOptions.tags.flatMap((tag) => ["--tag", tag]) : [],
|
|
51298
|
+
...input.runOptions.priority ? ["--priority", input.runOptions.priority] : [],
|
|
51299
|
+
...input.runOptions.projectId ? ["--project", input.runOptions.projectId] : [],
|
|
51300
|
+
...input.runOptions.model ? ["--model", input.runOptions.model] : [],
|
|
51301
|
+
...input.runOptions.headed ? ["--headed"] : [],
|
|
51302
|
+
...input.runOptions.parallel ? ["--parallel", String(input.runOptions.parallel)] : [],
|
|
51303
|
+
...input.runOptions.timeout ? ["--timeout", String(input.runOptions.timeout)] : [],
|
|
51304
|
+
...input.runOptions.personaIds?.length ? ["--persona", input.runOptions.personaIds.join(",")] : [],
|
|
51305
|
+
"--no-auto-generate",
|
|
51306
|
+
"--json"
|
|
51307
|
+
];
|
|
51308
|
+
return [
|
|
51309
|
+
"set -euo pipefail",
|
|
51310
|
+
`mkdir -p ${shellQuote(input.remoteDir)}`,
|
|
51311
|
+
`cd ${shellQuote(input.remoteDir)}`,
|
|
51312
|
+
input.setupCommand,
|
|
51313
|
+
`HASNA_TESTERS_DB_PATH=${shellQuote(input.dbPath)} ${args.map(shellQuote).join(" ")}`
|
|
51314
|
+
].filter(Boolean).join(`
|
|
51315
|
+
`);
|
|
51316
|
+
}
|
|
51317
|
+
async function runViaSandbox(plan, dependencies) {
|
|
51318
|
+
if (!plan.sandbox)
|
|
51319
|
+
throw new Error("Workflow does not have a sandbox plan");
|
|
51320
|
+
const sandboxes = await resolveSandboxesRuntime(dependencies);
|
|
51321
|
+
const createBundle = dependencies.createDatabaseBundle ?? createWorkflowDatabaseBundle;
|
|
51322
|
+
const bundle = createBundle(plan.workflow, plan);
|
|
51323
|
+
try {
|
|
51324
|
+
const raw = await sandboxes.runCommandInSandbox({
|
|
51325
|
+
command: plan.sandbox.command,
|
|
51326
|
+
provider: plan.sandbox.provider,
|
|
51327
|
+
name: plan.sandbox.name,
|
|
51328
|
+
image: plan.sandbox.image,
|
|
51329
|
+
sandboxTimeout: plan.sandbox.timeoutMs,
|
|
51330
|
+
commandTimeoutMs: plan.sandbox.timeoutMs,
|
|
51331
|
+
projectId: plan.workflow.projectId ?? undefined,
|
|
51332
|
+
config: {
|
|
51333
|
+
source: "testers",
|
|
51334
|
+
workflowId: plan.workflow.id,
|
|
51335
|
+
workflowName: plan.workflow.name
|
|
51336
|
+
},
|
|
51337
|
+
sandboxEnvVars: plan.sandbox.env,
|
|
51338
|
+
cleanup: plan.sandbox.cleanup,
|
|
51339
|
+
upload: {
|
|
51340
|
+
localDir: bundle.localDir,
|
|
51341
|
+
remoteDir: bundle.remoteDir
|
|
51342
|
+
}
|
|
51343
|
+
});
|
|
51344
|
+
const exitCode = raw.result.exit_code ?? raw.result.exitCode ?? 0;
|
|
51345
|
+
const stdout = raw.result.stdout ?? "";
|
|
51346
|
+
const stderr = raw.result.stderr ?? "";
|
|
51347
|
+
if (exitCode !== 0) {
|
|
51348
|
+
throw new Error(`Sandbox workflow execution failed (${exitCode}): ${stderr || stdout}`);
|
|
51349
|
+
}
|
|
51350
|
+
return {
|
|
51351
|
+
sandboxId: raw.sandbox.id,
|
|
51352
|
+
sessionId: raw.session.id,
|
|
51353
|
+
exitCode,
|
|
51354
|
+
stdout,
|
|
51355
|
+
stderr,
|
|
51356
|
+
cleanup: raw.cleanup
|
|
51357
|
+
};
|
|
51358
|
+
} finally {
|
|
51359
|
+
bundle.cleanup?.();
|
|
50850
51360
|
}
|
|
50851
|
-
|
|
51361
|
+
}
|
|
51362
|
+
async function resolveSandboxesRuntime(dependencies) {
|
|
51363
|
+
if (dependencies.sandboxes)
|
|
51364
|
+
return dependencies.sandboxes;
|
|
51365
|
+
if (dependencies.createSandboxesSDK)
|
|
51366
|
+
return dependencies.createSandboxesSDK();
|
|
51367
|
+
const mod = await import("@hasna/sandboxes");
|
|
51368
|
+
return mod.createSandboxesSDK();
|
|
51369
|
+
}
|
|
51370
|
+
function shellQuote(value) {
|
|
51371
|
+
return `'${value.replaceAll("'", `'"'"'`)}'`;
|
|
50852
51372
|
}
|
|
50853
51373
|
|
|
50854
51374
|
// src/lib/workflow-agent.ts
|
|
@@ -51200,10 +51720,16 @@ var WorkflowFilterSchema = exports_external.object({
|
|
|
51200
51720
|
priority: exports_external.enum(["low", "medium", "high", "critical"]).optional()
|
|
51201
51721
|
}).optional();
|
|
51202
51722
|
var WorkflowExecutionSchema = exports_external.object({
|
|
51203
|
-
target: exports_external.enum(["local", "connector:e2b"]).default("local"),
|
|
51723
|
+
target: exports_external.enum(["local", "sandbox", "connector:e2b"]).default("local"),
|
|
51204
51724
|
connector: exports_external.string().optional(),
|
|
51205
51725
|
operation: exports_external.string().optional(),
|
|
51726
|
+
provider: exports_external.string().optional(),
|
|
51727
|
+
sandboxImage: exports_external.string().optional(),
|
|
51728
|
+
sandboxRemoteDir: exports_external.string().optional(),
|
|
51729
|
+
sandboxCleanup: exports_external.enum(["delete", "stop", "keep"]).optional(),
|
|
51206
51730
|
sandboxTemplate: exports_external.string().optional(),
|
|
51731
|
+
setupCommand: exports_external.string().optional(),
|
|
51732
|
+
packageSpec: exports_external.string().optional(),
|
|
51207
51733
|
timeoutMs: exports_external.number().int().positive().optional(),
|
|
51208
51734
|
env: exports_external.record(exports_external.string()).optional()
|
|
51209
51735
|
}).optional();
|
|
@@ -51285,7 +51811,7 @@ async function handleRequest(req) {
|
|
|
51285
51811
|
if (pathname === "/api/status" && method === "GET") {
|
|
51286
51812
|
const config2 = loadConfig();
|
|
51287
51813
|
getDatabase();
|
|
51288
|
-
const dbPath = process.env["HASNA_TESTERS_DB_PATH"] ?? process.env["TESTERS_DB_PATH"] ??
|
|
51814
|
+
const dbPath = process.env["HASNA_TESTERS_DB_PATH"] ?? process.env["TESTERS_DB_PATH"] ?? join14(getTestersDir(), "testers.db");
|
|
51289
51815
|
const scenarios = listScenarios();
|
|
51290
51816
|
const runs = listRuns();
|
|
51291
51817
|
return jsonResponse({
|
|
@@ -52015,7 +52541,7 @@ async function handleRequest(req) {
|
|
|
52015
52541
|
return jsonResponse({ routes, apiRoutes, totalCovered: coverageMap.size });
|
|
52016
52542
|
}
|
|
52017
52543
|
if (!pathname.startsWith("/api")) {
|
|
52018
|
-
const dashboardDir =
|
|
52544
|
+
const dashboardDir = join14(import.meta.dir, "..", "..", "dashboard", "dist");
|
|
52019
52545
|
if (!existsSync10(dashboardDir)) {
|
|
52020
52546
|
return new Response(`<!DOCTYPE html>
|
|
52021
52547
|
<html>
|
|
@@ -52034,7 +52560,7 @@ async function handleRequest(req) {
|
|
|
52034
52560
|
}
|
|
52035
52561
|
});
|
|
52036
52562
|
}
|
|
52037
|
-
const filePath =
|
|
52563
|
+
const filePath = join14(dashboardDir, pathname === "/" ? "index.html" : pathname);
|
|
52038
52564
|
if (existsSync10(filePath)) {
|
|
52039
52565
|
const file2 = Bun.file(filePath);
|
|
52040
52566
|
return new Response(file2, {
|
|
@@ -52044,7 +52570,7 @@ async function handleRequest(req) {
|
|
|
52044
52570
|
}
|
|
52045
52571
|
});
|
|
52046
52572
|
}
|
|
52047
|
-
const indexPath =
|
|
52573
|
+
const indexPath = join14(dashboardDir, "index.html");
|
|
52048
52574
|
if (existsSync10(indexPath)) {
|
|
52049
52575
|
const file2 = Bun.file(indexPath);
|
|
52050
52576
|
return new Response(file2, {
|