@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/cli/index.js
CHANGED
|
@@ -2100,6 +2100,56 @@ var require_commander = __commonJS((exports) => {
|
|
|
2100
2100
|
});
|
|
2101
2101
|
|
|
2102
2102
|
// src/types/index.ts
|
|
2103
|
+
function isRecord(value) {
|
|
2104
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2105
|
+
}
|
|
2106
|
+
function stringValue(value) {
|
|
2107
|
+
return typeof value === "string" && value.trim() ? value : undefined;
|
|
2108
|
+
}
|
|
2109
|
+
function numberValue(value) {
|
|
2110
|
+
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
2111
|
+
}
|
|
2112
|
+
function stringMap(value) {
|
|
2113
|
+
if (!isRecord(value))
|
|
2114
|
+
return;
|
|
2115
|
+
const entries = Object.entries(value).filter((entry) => typeof entry[1] === "string");
|
|
2116
|
+
return entries.length > 0 ? Object.fromEntries(entries) : undefined;
|
|
2117
|
+
}
|
|
2118
|
+
function cleanupValue(value) {
|
|
2119
|
+
if (value === "delete" || value === "stop" || value === "keep")
|
|
2120
|
+
return value;
|
|
2121
|
+
return;
|
|
2122
|
+
}
|
|
2123
|
+
function workflowExecutionFromValue(value) {
|
|
2124
|
+
const input = isRecord(value) ? value : {};
|
|
2125
|
+
const rawTarget = stringValue(input["target"]) ?? "local";
|
|
2126
|
+
if (rawTarget === "local") {
|
|
2127
|
+
const timeoutMs2 = numberValue(input["timeoutMs"]);
|
|
2128
|
+
return timeoutMs2 === undefined ? { target: "local" } : { target: "local", timeoutMs: timeoutMs2 };
|
|
2129
|
+
}
|
|
2130
|
+
if (rawTarget !== "sandbox" && rawTarget !== "connector:e2b") {
|
|
2131
|
+
throw new Error(`Unsupported workflow execution target: ${rawTarget}`);
|
|
2132
|
+
}
|
|
2133
|
+
const provider = rawTarget === "connector:e2b" ? "e2b" : stringValue(input["provider"]) ?? stringValue(input["connector"]);
|
|
2134
|
+
const sandboxImage = stringValue(input["sandboxImage"]) ?? stringValue(input["sandboxTemplate"]);
|
|
2135
|
+
const sandboxRemoteDir = stringValue(input["sandboxRemoteDir"]);
|
|
2136
|
+
const sandboxCleanup = cleanupValue(input["sandboxCleanup"]);
|
|
2137
|
+
const setupCommand = stringValue(input["setupCommand"]);
|
|
2138
|
+
const packageSpec = stringValue(input["packageSpec"]);
|
|
2139
|
+
const timeoutMs = numberValue(input["timeoutMs"]);
|
|
2140
|
+
const env = stringMap(input["env"]);
|
|
2141
|
+
return {
|
|
2142
|
+
target: "sandbox",
|
|
2143
|
+
...provider ? { provider } : {},
|
|
2144
|
+
...sandboxImage ? { sandboxImage } : {},
|
|
2145
|
+
...sandboxRemoteDir ? { sandboxRemoteDir } : {},
|
|
2146
|
+
...sandboxCleanup ? { sandboxCleanup } : {},
|
|
2147
|
+
...setupCommand ? { setupCommand } : {},
|
|
2148
|
+
...packageSpec ? { packageSpec } : {},
|
|
2149
|
+
...timeoutMs !== undefined ? { timeoutMs } : {},
|
|
2150
|
+
...env ? { env } : {}
|
|
2151
|
+
};
|
|
2152
|
+
}
|
|
2103
2153
|
function workflowFromRow(row) {
|
|
2104
2154
|
return {
|
|
2105
2155
|
id: row.id,
|
|
@@ -2109,7 +2159,7 @@ function workflowFromRow(row) {
|
|
|
2109
2159
|
scenarioFilter: JSON.parse(row.scenario_filter || "{}"),
|
|
2110
2160
|
personaIds: JSON.parse(row.persona_ids || "[]"),
|
|
2111
2161
|
goal: row.goal ? JSON.parse(row.goal) : null,
|
|
2112
|
-
execution: JSON.parse(row.execution || '{"target":"local"}'),
|
|
2162
|
+
execution: workflowExecutionFromValue(JSON.parse(row.execution || '{"target":"local"}')),
|
|
2113
2163
|
settings: JSON.parse(row.settings || "{}"),
|
|
2114
2164
|
enabled: row.enabled === 1,
|
|
2115
2165
|
createdAt: row.created_at,
|
|
@@ -14074,6 +14124,7 @@ __export(exports_ai_client, {
|
|
|
14074
14124
|
createClientForModel: () => createClientForModel,
|
|
14075
14125
|
createClient: () => createClient,
|
|
14076
14126
|
callOpenAICompatible: () => callOpenAICompatible,
|
|
14127
|
+
buildScenarioUserMessage: () => buildScenarioUserMessage,
|
|
14077
14128
|
BROWSER_TOOLS: () => BROWSER_TOOLS
|
|
14078
14129
|
});
|
|
14079
14130
|
import Anthropic2 from "@anthropic-ai/sdk";
|
|
@@ -14486,7 +14537,6 @@ async function executeTool(page, screenshotter, toolName, toolInput, context) {
|
|
|
14486
14537
|
const assertionType = toolInput.assertion_type;
|
|
14487
14538
|
const selector = toolInput.selector;
|
|
14488
14539
|
const expected = toolInput.expected;
|
|
14489
|
-
const sessionId = context.sessionId ?? "default";
|
|
14490
14540
|
switch (assertionType) {
|
|
14491
14541
|
case "element_exists": {
|
|
14492
14542
|
if (!selector)
|
|
@@ -14551,7 +14601,6 @@ async function executeTool(page, screenshotter, toolName, toolInput, context) {
|
|
|
14551
14601
|
case "browser_intercept": {
|
|
14552
14602
|
const action = toolInput.action;
|
|
14553
14603
|
const pattern = toolInput.pattern;
|
|
14554
|
-
const interceptAction = toolInput.intercept_action;
|
|
14555
14604
|
const statusCode = toolInput.status_code;
|
|
14556
14605
|
const body = toolInput.body;
|
|
14557
14606
|
const sessionId = context.sessionId ?? "default";
|
|
@@ -14628,7 +14677,28 @@ ${JSON.stringify(har, null, 2)}` };
|
|
|
14628
14677
|
}
|
|
14629
14678
|
case "browser_a11y": {
|
|
14630
14679
|
const level = toolInput.level ?? "AA";
|
|
14631
|
-
const snapshot = await page.
|
|
14680
|
+
const snapshot = await page.evaluate(() => {
|
|
14681
|
+
function readRole(el) {
|
|
14682
|
+
return el.getAttribute("role") ?? el.tagName.toLowerCase();
|
|
14683
|
+
}
|
|
14684
|
+
function readName(el) {
|
|
14685
|
+
const labelledBy = el.getAttribute("aria-labelledby");
|
|
14686
|
+
if (labelledBy) {
|
|
14687
|
+
const labelledText = labelledBy.split(/\s+/).map((id) => document.getElementById(id)?.textContent?.trim()).filter(Boolean).join(" ");
|
|
14688
|
+
if (labelledText)
|
|
14689
|
+
return labelledText;
|
|
14690
|
+
}
|
|
14691
|
+
return el.getAttribute("aria-label") ?? el.getAttribute("alt") ?? el.textContent?.trim() ?? "";
|
|
14692
|
+
}
|
|
14693
|
+
function walk(el) {
|
|
14694
|
+
return {
|
|
14695
|
+
role: readRole(el),
|
|
14696
|
+
name: readName(el),
|
|
14697
|
+
children: Array.from(el.children).map((child) => walk(child))
|
|
14698
|
+
};
|
|
14699
|
+
}
|
|
14700
|
+
return document.body ? walk(document.body) : null;
|
|
14701
|
+
});
|
|
14632
14702
|
if (!snapshot)
|
|
14633
14703
|
return { result: "Error: could not capture accessibility tree" };
|
|
14634
14704
|
const issues = [];
|
|
@@ -14670,6 +14740,38 @@ ${filtered.join(`
|
|
|
14670
14740
|
return { result: `Error executing ${toolName}: ${message}` };
|
|
14671
14741
|
}
|
|
14672
14742
|
}
|
|
14743
|
+
function resolveStartUrl(baseUrl, targetPath) {
|
|
14744
|
+
try {
|
|
14745
|
+
return new URL(targetPath, baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`).toString();
|
|
14746
|
+
} catch {
|
|
14747
|
+
return `${baseUrl.replace(/\/+$/, "")}/${targetPath.replace(/^\/+/, "")}`;
|
|
14748
|
+
}
|
|
14749
|
+
}
|
|
14750
|
+
function buildScenarioUserMessage(scenario, baseUrl) {
|
|
14751
|
+
const userParts = [
|
|
14752
|
+
`**Scenario:** ${scenario.name}`,
|
|
14753
|
+
`**Description:** ${scenario.description}`
|
|
14754
|
+
];
|
|
14755
|
+
if (baseUrl) {
|
|
14756
|
+
const normalizedBaseUrl = baseUrl.replace(/\/+$/, "");
|
|
14757
|
+
userParts.push(`**Base URL:** ${normalizedBaseUrl}`);
|
|
14758
|
+
if (scenario.targetPath) {
|
|
14759
|
+
userParts.push(`**Start URL:** ${resolveStartUrl(normalizedBaseUrl, scenario.targetPath)}`);
|
|
14760
|
+
}
|
|
14761
|
+
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.");
|
|
14762
|
+
}
|
|
14763
|
+
if (scenario.targetPath) {
|
|
14764
|
+
userParts.push(`**Target Path:** ${scenario.targetPath}`);
|
|
14765
|
+
}
|
|
14766
|
+
if (scenario.steps.length > 0) {
|
|
14767
|
+
userParts.push("**Steps:**");
|
|
14768
|
+
for (let i = 0;i < scenario.steps.length; i++) {
|
|
14769
|
+
userParts.push(`${i + 1}. ${scenario.steps[i]}`);
|
|
14770
|
+
}
|
|
14771
|
+
}
|
|
14772
|
+
return userParts.join(`
|
|
14773
|
+
`);
|
|
14774
|
+
}
|
|
14673
14775
|
async function runAgentLoop(options) {
|
|
14674
14776
|
const {
|
|
14675
14777
|
client,
|
|
@@ -14679,6 +14781,7 @@ async function runAgentLoop(options) {
|
|
|
14679
14781
|
model,
|
|
14680
14782
|
runId,
|
|
14681
14783
|
sessionId,
|
|
14784
|
+
baseUrl,
|
|
14682
14785
|
maxTurns = 30,
|
|
14683
14786
|
onStep,
|
|
14684
14787
|
persona,
|
|
@@ -14726,21 +14829,7 @@ Instructions: ${persona.instructions}` : "",
|
|
|
14726
14829
|
"- Verify both positive and negative states"
|
|
14727
14830
|
].join(`
|
|
14728
14831
|
`) + personaSection;
|
|
14729
|
-
const
|
|
14730
|
-
`**Scenario:** ${scenario.name}`,
|
|
14731
|
-
`**Description:** ${scenario.description}`
|
|
14732
|
-
];
|
|
14733
|
-
if (scenario.targetPath) {
|
|
14734
|
-
userParts.push(`**Target Path:** ${scenario.targetPath}`);
|
|
14735
|
-
}
|
|
14736
|
-
if (scenario.steps.length > 0) {
|
|
14737
|
-
userParts.push("**Steps:**");
|
|
14738
|
-
for (let i = 0;i < scenario.steps.length; i++) {
|
|
14739
|
-
userParts.push(`${i + 1}. ${scenario.steps[i]}`);
|
|
14740
|
-
}
|
|
14741
|
-
}
|
|
14742
|
-
const userMessage = userParts.join(`
|
|
14743
|
-
`);
|
|
14832
|
+
const userMessage = buildScenarioUserMessage(scenario, baseUrl);
|
|
14744
14833
|
const screenshots = [];
|
|
14745
14834
|
let tokensUsed = 0;
|
|
14746
14835
|
let stepNumber = 0;
|
|
@@ -14803,7 +14892,7 @@ Instructions: ${persona.instructions}` : "",
|
|
|
14803
14892
|
if (onStep) {
|
|
14804
14893
|
onStep({ type: "tool_call", toolName: toolBlock.name, toolInput, stepNumber });
|
|
14805
14894
|
}
|
|
14806
|
-
const execResult = await executeTool(page, screenshotter, toolBlock.name, toolInput, { runId, scenarioSlug, stepNumber, sessionId, a11y });
|
|
14895
|
+
const execResult = await executeTool(page, screenshotter, toolBlock.name, toolInput, { runId, scenarioSlug, stepNumber, sessionId: sessionId ?? runId, a11y });
|
|
14807
14896
|
if (onStep) {
|
|
14808
14897
|
onStep({ type: "tool_result", toolName: toolBlock.name, toolResult: execResult.result, stepNumber });
|
|
14809
14898
|
}
|
|
@@ -17494,6 +17583,381 @@ var init_failure_pipeline = __esm(() => {
|
|
|
17494
17583
|
init_todos_connector();
|
|
17495
17584
|
});
|
|
17496
17585
|
|
|
17586
|
+
// src/lib/a11y-audit.ts
|
|
17587
|
+
async function runA11yAudit(page, options = {}) {
|
|
17588
|
+
const { level = "AA", rules, exclude = [] } = options;
|
|
17589
|
+
await page.addScriptTag({ url: "https://cdnjs.cloudflare.com/ajax/libs/axe-core/4.9.1/axe.min.js" });
|
|
17590
|
+
const config = {
|
|
17591
|
+
runOnly: {
|
|
17592
|
+
type: level === "AAA" ? "standard" : "tag",
|
|
17593
|
+
values: level === "AAA" ? undefined : [level, "best-practice"]
|
|
17594
|
+
}
|
|
17595
|
+
};
|
|
17596
|
+
if (rules && rules.length > 0) {
|
|
17597
|
+
config.rules = Object.fromEntries(rules.map((r) => [r, { enabled: true }]));
|
|
17598
|
+
}
|
|
17599
|
+
if (exclude.length > 0) {
|
|
17600
|
+
config.exclude = exclude;
|
|
17601
|
+
}
|
|
17602
|
+
const result = await page.evaluate(async (auditConfig) => {
|
|
17603
|
+
const axeResult = await window.axe.run(auditConfig);
|
|
17604
|
+
return axeResult;
|
|
17605
|
+
}, config);
|
|
17606
|
+
const violations = (result.violations ?? []).map((v) => ({
|
|
17607
|
+
id: v.id,
|
|
17608
|
+
impact: v.impact,
|
|
17609
|
+
description: v.description,
|
|
17610
|
+
help: v.help,
|
|
17611
|
+
helpUrl: v.helpUrl,
|
|
17612
|
+
nodes: (v.nodes ?? []).map((n) => ({
|
|
17613
|
+
html: n.html,
|
|
17614
|
+
target: n.target,
|
|
17615
|
+
failureSummary: n.failureSummary
|
|
17616
|
+
}))
|
|
17617
|
+
}));
|
|
17618
|
+
const passes = (result.passes ?? []).map((p) => ({
|
|
17619
|
+
id: p.id,
|
|
17620
|
+
description: p.description
|
|
17621
|
+
}));
|
|
17622
|
+
const incomplete = (result.incomplete ?? []).map((i) => ({
|
|
17623
|
+
id: i.id,
|
|
17624
|
+
description: i.description,
|
|
17625
|
+
impact: i.impact
|
|
17626
|
+
}));
|
|
17627
|
+
const criticalCount = violations.filter((v) => v.impact === "critical").length;
|
|
17628
|
+
const seriousCount = violations.filter((v) => v.impact === "serious").length;
|
|
17629
|
+
const moderateCount = violations.filter((v) => v.impact === "moderate").length;
|
|
17630
|
+
const minorCount = violations.filter((v) => v.impact === "minor").length;
|
|
17631
|
+
return {
|
|
17632
|
+
violations,
|
|
17633
|
+
passes,
|
|
17634
|
+
incomplete,
|
|
17635
|
+
url: page.url(),
|
|
17636
|
+
timestamp: new Date().toISOString(),
|
|
17637
|
+
totalViolations: violations.length,
|
|
17638
|
+
criticalCount,
|
|
17639
|
+
seriousCount,
|
|
17640
|
+
moderateCount,
|
|
17641
|
+
minorCount
|
|
17642
|
+
};
|
|
17643
|
+
}
|
|
17644
|
+
|
|
17645
|
+
// src/lib/assertions.ts
|
|
17646
|
+
async function evaluateAssertions(page, assertions, context = {}) {
|
|
17647
|
+
const results = [];
|
|
17648
|
+
for (const assertion of assertions) {
|
|
17649
|
+
try {
|
|
17650
|
+
const result = await evaluateOne(page, assertion, context);
|
|
17651
|
+
results.push(result);
|
|
17652
|
+
} catch (err) {
|
|
17653
|
+
results.push({
|
|
17654
|
+
assertion,
|
|
17655
|
+
passed: false,
|
|
17656
|
+
actual: "",
|
|
17657
|
+
error: err instanceof Error ? err.message : String(err)
|
|
17658
|
+
});
|
|
17659
|
+
}
|
|
17660
|
+
}
|
|
17661
|
+
return results;
|
|
17662
|
+
}
|
|
17663
|
+
async function evaluateOne(page, assertion, context) {
|
|
17664
|
+
switch (assertion.type) {
|
|
17665
|
+
case "visible": {
|
|
17666
|
+
const visible = await page.locator(assertion.selector).isVisible();
|
|
17667
|
+
return {
|
|
17668
|
+
assertion,
|
|
17669
|
+
passed: visible,
|
|
17670
|
+
actual: String(visible)
|
|
17671
|
+
};
|
|
17672
|
+
}
|
|
17673
|
+
case "not_visible": {
|
|
17674
|
+
const visible = await page.locator(assertion.selector).isVisible();
|
|
17675
|
+
return {
|
|
17676
|
+
assertion,
|
|
17677
|
+
passed: !visible,
|
|
17678
|
+
actual: String(visible)
|
|
17679
|
+
};
|
|
17680
|
+
}
|
|
17681
|
+
case "text_contains": {
|
|
17682
|
+
const text = await page.locator(assertion.selector).textContent() ?? "";
|
|
17683
|
+
const expected = String(assertion.expected ?? "");
|
|
17684
|
+
return {
|
|
17685
|
+
assertion,
|
|
17686
|
+
passed: text.includes(expected),
|
|
17687
|
+
actual: text
|
|
17688
|
+
};
|
|
17689
|
+
}
|
|
17690
|
+
case "text_equals": {
|
|
17691
|
+
const text = await page.locator(assertion.selector).textContent() ?? "";
|
|
17692
|
+
const expected = String(assertion.expected ?? "");
|
|
17693
|
+
return {
|
|
17694
|
+
assertion,
|
|
17695
|
+
passed: text.trim() === expected.trim(),
|
|
17696
|
+
actual: text
|
|
17697
|
+
};
|
|
17698
|
+
}
|
|
17699
|
+
case "element_count": {
|
|
17700
|
+
const count = await page.locator(assertion.selector).count();
|
|
17701
|
+
const expected = Number(assertion.expected ?? 0);
|
|
17702
|
+
return {
|
|
17703
|
+
assertion,
|
|
17704
|
+
passed: count === expected,
|
|
17705
|
+
actual: String(count)
|
|
17706
|
+
};
|
|
17707
|
+
}
|
|
17708
|
+
case "no_console_errors": {
|
|
17709
|
+
if (context.consoleErrors !== undefined) {
|
|
17710
|
+
const errors = context.consoleErrors.filter(Boolean);
|
|
17711
|
+
return {
|
|
17712
|
+
assertion,
|
|
17713
|
+
passed: errors.length === 0,
|
|
17714
|
+
actual: errors.length === 0 ? "No console errors captured" : errors.slice(0, 3).join(" | ")
|
|
17715
|
+
};
|
|
17716
|
+
}
|
|
17717
|
+
const errorElements = await page.locator('[role="alert"], .error, .error-message, [data-testid="error"]').count();
|
|
17718
|
+
return {
|
|
17719
|
+
assertion,
|
|
17720
|
+
passed: errorElements === 0,
|
|
17721
|
+
actual: `${errorElements} error element(s) found`
|
|
17722
|
+
};
|
|
17723
|
+
}
|
|
17724
|
+
case "no_a11y_violations": {
|
|
17725
|
+
try {
|
|
17726
|
+
const auditResult = await runA11yAudit(page);
|
|
17727
|
+
const hasIssues = auditResult.violations.length > 0;
|
|
17728
|
+
return {
|
|
17729
|
+
assertion,
|
|
17730
|
+
passed: !hasIssues,
|
|
17731
|
+
actual: hasIssues ? `${auditResult.totalViolations} violation(s): ${auditResult.violations.map((v) => v.id).join(", ")}` : "No accessibility violations found"
|
|
17732
|
+
};
|
|
17733
|
+
} catch (err) {
|
|
17734
|
+
return {
|
|
17735
|
+
assertion,
|
|
17736
|
+
passed: false,
|
|
17737
|
+
actual: "",
|
|
17738
|
+
error: err instanceof Error ? err.message : String(err)
|
|
17739
|
+
};
|
|
17740
|
+
}
|
|
17741
|
+
}
|
|
17742
|
+
case "url_contains": {
|
|
17743
|
+
const url = page.url();
|
|
17744
|
+
const expected = String(assertion.expected ?? "");
|
|
17745
|
+
return {
|
|
17746
|
+
assertion,
|
|
17747
|
+
passed: url.includes(expected),
|
|
17748
|
+
actual: url
|
|
17749
|
+
};
|
|
17750
|
+
}
|
|
17751
|
+
case "title_contains": {
|
|
17752
|
+
const title = await page.title();
|
|
17753
|
+
const expected = String(assertion.expected ?? "");
|
|
17754
|
+
return {
|
|
17755
|
+
assertion,
|
|
17756
|
+
passed: title.includes(expected),
|
|
17757
|
+
actual: title
|
|
17758
|
+
};
|
|
17759
|
+
}
|
|
17760
|
+
case "cookie_exists": {
|
|
17761
|
+
const cookieName = assertion.expected;
|
|
17762
|
+
const cookies = await page.context().cookies();
|
|
17763
|
+
const found = cookies.some((c) => c.name === cookieName);
|
|
17764
|
+
return {
|
|
17765
|
+
assertion,
|
|
17766
|
+
passed: found,
|
|
17767
|
+
actual: found ? `Cookie "${cookieName}" exists` : `Cookie "${cookieName}" not found`
|
|
17768
|
+
};
|
|
17769
|
+
}
|
|
17770
|
+
case "cookie_not_exists": {
|
|
17771
|
+
const cookieName = assertion.expected;
|
|
17772
|
+
const cookies = await page.context().cookies();
|
|
17773
|
+
const found = cookies.some((c) => c.name === cookieName);
|
|
17774
|
+
return {
|
|
17775
|
+
assertion,
|
|
17776
|
+
passed: !found,
|
|
17777
|
+
actual: found ? `Cookie "${cookieName}" found (unexpected)` : `Cookie "${cookieName}" does not exist`
|
|
17778
|
+
};
|
|
17779
|
+
}
|
|
17780
|
+
case "cookie_value": {
|
|
17781
|
+
const [cookieName, expectedValue] = assertion.expected.split("=", 2);
|
|
17782
|
+
const cookies = await page.context().cookies();
|
|
17783
|
+
const cookie = cookies.find((c) => c.name === cookieName);
|
|
17784
|
+
const actualValue = cookie?.value ?? "";
|
|
17785
|
+
return {
|
|
17786
|
+
assertion,
|
|
17787
|
+
passed: actualValue === expectedValue,
|
|
17788
|
+
actual: cookie ? `${cookieName}=${actualValue}` : `Cookie "${cookieName}" not found`
|
|
17789
|
+
};
|
|
17790
|
+
}
|
|
17791
|
+
case "local_storage_exists": {
|
|
17792
|
+
const key = assertion.expected;
|
|
17793
|
+
const value = await page.evaluate((k) => localStorage.getItem(k), key);
|
|
17794
|
+
return {
|
|
17795
|
+
assertion,
|
|
17796
|
+
passed: value !== null,
|
|
17797
|
+
actual: value !== null ? `Key "${key}" exists with value "${value}"` : `Key "${key}" not found in localStorage`
|
|
17798
|
+
};
|
|
17799
|
+
}
|
|
17800
|
+
case "local_storage_not_exists": {
|
|
17801
|
+
const key = assertion.expected;
|
|
17802
|
+
const value = await page.evaluate((k) => localStorage.getItem(k), key);
|
|
17803
|
+
return {
|
|
17804
|
+
assertion,
|
|
17805
|
+
passed: value === null,
|
|
17806
|
+
actual: value !== null ? `Key "${key}" exists (unexpected)` : `Key "${key}" does not exist in localStorage`
|
|
17807
|
+
};
|
|
17808
|
+
}
|
|
17809
|
+
case "local_storage_value": {
|
|
17810
|
+
const [lsKey, expectedValue] = assertion.expected.split("=", 2);
|
|
17811
|
+
const value = await page.evaluate((k) => localStorage.getItem(k), lsKey ?? "");
|
|
17812
|
+
return {
|
|
17813
|
+
assertion,
|
|
17814
|
+
passed: value === expectedValue,
|
|
17815
|
+
actual: value !== null ? `${lsKey}=${value}` : `Key "${lsKey}" not found in localStorage`
|
|
17816
|
+
};
|
|
17817
|
+
}
|
|
17818
|
+
case "session_storage_value": {
|
|
17819
|
+
const [ssKey, expectedValue] = assertion.expected.split("=", 2);
|
|
17820
|
+
const value = await page.evaluate((k) => sessionStorage.getItem(k), ssKey ?? "");
|
|
17821
|
+
return {
|
|
17822
|
+
assertion,
|
|
17823
|
+
passed: value === expectedValue,
|
|
17824
|
+
actual: value !== null ? `${ssKey}=${value}` : `Key "${ssKey}" not found in sessionStorage`
|
|
17825
|
+
};
|
|
17826
|
+
}
|
|
17827
|
+
case "session_storage_not_exists": {
|
|
17828
|
+
const key = assertion.expected;
|
|
17829
|
+
const value = await page.evaluate((k) => sessionStorage.getItem(k), key);
|
|
17830
|
+
return {
|
|
17831
|
+
assertion,
|
|
17832
|
+
passed: value === null,
|
|
17833
|
+
actual: value !== null ? `Key "${key}" exists (unexpected)` : `Key "${key}" does not exist in sessionStorage`
|
|
17834
|
+
};
|
|
17835
|
+
}
|
|
17836
|
+
default: {
|
|
17837
|
+
return {
|
|
17838
|
+
assertion,
|
|
17839
|
+
passed: false,
|
|
17840
|
+
actual: "",
|
|
17841
|
+
error: `Unknown assertion type: ${assertion.type}`
|
|
17842
|
+
};
|
|
17843
|
+
}
|
|
17844
|
+
}
|
|
17845
|
+
}
|
|
17846
|
+
function parseAssertionString(str) {
|
|
17847
|
+
const trimmed = str.trim();
|
|
17848
|
+
if (trimmed === "no-console-errors") {
|
|
17849
|
+
return { type: "no_console_errors", description: "No console errors" };
|
|
17850
|
+
}
|
|
17851
|
+
if (trimmed.startsWith("url:contains:")) {
|
|
17852
|
+
const expected = trimmed.slice("url:contains:".length);
|
|
17853
|
+
return { type: "url_contains", expected, description: `URL contains "${expected}"` };
|
|
17854
|
+
}
|
|
17855
|
+
if (trimmed.startsWith("title:contains:")) {
|
|
17856
|
+
const expected = trimmed.slice("title:contains:".length);
|
|
17857
|
+
return { type: "title_contains", expected, description: `Title contains "${expected}"` };
|
|
17858
|
+
}
|
|
17859
|
+
if (trimmed.startsWith("count:")) {
|
|
17860
|
+
const rest = trimmed.slice("count:".length);
|
|
17861
|
+
const eqIdx = rest.indexOf(" eq:");
|
|
17862
|
+
if (eqIdx === -1) {
|
|
17863
|
+
throw new Error(`Invalid count assertion format: ${str}. Expected "count:<selector> eq:<number>"`);
|
|
17864
|
+
}
|
|
17865
|
+
const selector = rest.slice(0, eqIdx);
|
|
17866
|
+
const expected = parseInt(rest.slice(eqIdx + " eq:".length), 10);
|
|
17867
|
+
return { type: "element_count", selector, expected, description: `${selector} count equals ${expected}` };
|
|
17868
|
+
}
|
|
17869
|
+
if (trimmed.startsWith("text:")) {
|
|
17870
|
+
const rest = trimmed.slice("text:".length);
|
|
17871
|
+
const containsIdx = rest.indexOf(" contains:");
|
|
17872
|
+
const equalsIdx = rest.indexOf(" equals:");
|
|
17873
|
+
if (containsIdx !== -1) {
|
|
17874
|
+
const selector = rest.slice(0, containsIdx);
|
|
17875
|
+
const expected = rest.slice(containsIdx + " contains:".length);
|
|
17876
|
+
return { type: "text_contains", selector, expected, description: `${selector} text contains "${expected}"` };
|
|
17877
|
+
}
|
|
17878
|
+
if (equalsIdx !== -1) {
|
|
17879
|
+
const selector = rest.slice(0, equalsIdx);
|
|
17880
|
+
const expected = rest.slice(equalsIdx + " equals:".length);
|
|
17881
|
+
return { type: "text_equals", selector, expected, description: `${selector} text equals "${expected}"` };
|
|
17882
|
+
}
|
|
17883
|
+
throw new Error(`Invalid text assertion format: ${str}. Expected "text:<selector> contains:<text>" or "text:<selector> equals:<text>"`);
|
|
17884
|
+
}
|
|
17885
|
+
if (trimmed.startsWith("selector:")) {
|
|
17886
|
+
const rest = trimmed.slice("selector:".length);
|
|
17887
|
+
const lastSpace = rest.lastIndexOf(" ");
|
|
17888
|
+
if (lastSpace === -1) {
|
|
17889
|
+
throw new Error(`Invalid selector assertion format: ${str}. Expected "selector:<selector> visible" or "selector:<selector> not-visible"`);
|
|
17890
|
+
}
|
|
17891
|
+
const selector = rest.slice(0, lastSpace);
|
|
17892
|
+
const action = rest.slice(lastSpace + 1);
|
|
17893
|
+
if (action === "visible") {
|
|
17894
|
+
return { type: "visible", selector, description: `${selector} is visible` };
|
|
17895
|
+
}
|
|
17896
|
+
if (action === "not-visible") {
|
|
17897
|
+
return { type: "not_visible", selector, description: `${selector} is not visible` };
|
|
17898
|
+
}
|
|
17899
|
+
throw new Error(`Unknown selector action: "${action}". Expected "visible" or "not-visible"`);
|
|
17900
|
+
}
|
|
17901
|
+
if (trimmed.startsWith("cookie:exists:")) {
|
|
17902
|
+
const name = trimmed.slice("cookie:exists:".length);
|
|
17903
|
+
return { type: "cookie_exists", expected: name, description: `Cookie "${name}" exists` };
|
|
17904
|
+
}
|
|
17905
|
+
if (trimmed.startsWith("cookie:not-exists:")) {
|
|
17906
|
+
const name = trimmed.slice("cookie:not-exists:".length);
|
|
17907
|
+
return { type: "cookie_not_exists", expected: name, description: `Cookie "${name}" does not exist` };
|
|
17908
|
+
}
|
|
17909
|
+
if (trimmed.startsWith("cookie:value:")) {
|
|
17910
|
+
const valueStr = trimmed.slice("cookie:value:".length);
|
|
17911
|
+
return { type: "cookie_value", expected: valueStr, description: `Cookie value is "${valueStr}"` };
|
|
17912
|
+
}
|
|
17913
|
+
if (trimmed.startsWith("local:exists:")) {
|
|
17914
|
+
const key = trimmed.slice("local:exists:".length);
|
|
17915
|
+
return { type: "local_storage_exists", expected: key, description: `LocalStorage key "${key}" exists` };
|
|
17916
|
+
}
|
|
17917
|
+
if (trimmed.startsWith("local:not-exists:")) {
|
|
17918
|
+
const key = trimmed.slice("local:not-exists:".length);
|
|
17919
|
+
return { type: "local_storage_not_exists", expected: key, description: `LocalStorage key "${key}" does not exist` };
|
|
17920
|
+
}
|
|
17921
|
+
if (trimmed.startsWith("local:value:")) {
|
|
17922
|
+
const valueStr = trimmed.slice("local:value:".length);
|
|
17923
|
+
return { type: "local_storage_value", expected: valueStr, description: `LocalStorage value is "${valueStr}"` };
|
|
17924
|
+
}
|
|
17925
|
+
if (trimmed.startsWith("session:value:")) {
|
|
17926
|
+
const valueStr = trimmed.slice("session:value:".length);
|
|
17927
|
+
return { type: "session_storage_value", expected: valueStr, description: `SessionStorage value is "${valueStr}"` };
|
|
17928
|
+
}
|
|
17929
|
+
if (trimmed.startsWith("session:not-exists:")) {
|
|
17930
|
+
const key = trimmed.slice("session:not-exists:".length);
|
|
17931
|
+
return { type: "session_storage_not_exists", expected: key, description: `SessionStorage key "${key}" does not exist` };
|
|
17932
|
+
}
|
|
17933
|
+
throw new Error(`Cannot parse assertion: "${str}". See --help for assertion formats.`);
|
|
17934
|
+
}
|
|
17935
|
+
function allAssertionsPassed(results) {
|
|
17936
|
+
return results.every((r) => r.passed);
|
|
17937
|
+
}
|
|
17938
|
+
function formatAssertionResults(results) {
|
|
17939
|
+
if (results.length === 0)
|
|
17940
|
+
return "No assertions.";
|
|
17941
|
+
const lines = [];
|
|
17942
|
+
for (const r of results) {
|
|
17943
|
+
const icon = r.passed ? "PASS" : "FAIL";
|
|
17944
|
+
const desc = r.assertion.description || `${r.assertion.type}${r.assertion.selector ? ` ${r.assertion.selector}` : ""}`;
|
|
17945
|
+
let line = ` [${icon}] ${desc}`;
|
|
17946
|
+
if (!r.passed) {
|
|
17947
|
+
line += ` (actual: ${r.actual})`;
|
|
17948
|
+
if (r.error)
|
|
17949
|
+
line += ` \u2014 ${r.error}`;
|
|
17950
|
+
}
|
|
17951
|
+
lines.push(line);
|
|
17952
|
+
}
|
|
17953
|
+
const passed = results.filter((r) => r.passed).length;
|
|
17954
|
+
lines.push(`
|
|
17955
|
+
${passed}/${results.length} assertions passed.`);
|
|
17956
|
+
return lines.join(`
|
|
17957
|
+
`);
|
|
17958
|
+
}
|
|
17959
|
+
var init_assertions = () => {};
|
|
17960
|
+
|
|
17497
17961
|
// src/db/flows.ts
|
|
17498
17962
|
var exports_flows = {};
|
|
17499
17963
|
__export(exports_flows, {
|
|
@@ -17652,7 +18116,9 @@ __export(exports_runner, {
|
|
|
17652
18116
|
runSingleScenario: () => runSingleScenario,
|
|
17653
18117
|
runByFilter: () => runByFilter,
|
|
17654
18118
|
runBatch: () => runBatch,
|
|
17655
|
-
|
|
18119
|
+
resolveScenariosForRun: () => resolveScenariosForRun,
|
|
18120
|
+
onRunEvent: () => onRunEvent,
|
|
18121
|
+
applyStructuredAssertionsToResult: () => applyStructuredAssertionsToResult
|
|
17656
18122
|
});
|
|
17657
18123
|
import { mkdirSync as mkdirSync8 } from "fs";
|
|
17658
18124
|
import { join as join13 } from "path";
|
|
@@ -17664,6 +18130,54 @@ function emit(event) {
|
|
|
17664
18130
|
if (eventHandler)
|
|
17665
18131
|
eventHandler(event);
|
|
17666
18132
|
}
|
|
18133
|
+
function assertionDescription(result) {
|
|
18134
|
+
return result.assertion.description || `${result.assertion.type}${result.assertion.selector ? ` ${result.assertion.selector}` : ""}`;
|
|
18135
|
+
}
|
|
18136
|
+
function summarizeAssertionResult(result) {
|
|
18137
|
+
const description = assertionDescription(result);
|
|
18138
|
+
if (result.passed)
|
|
18139
|
+
return description;
|
|
18140
|
+
const suffix = result.error ? `; ${result.error}` : "";
|
|
18141
|
+
return `${description} (actual: ${result.actual}${suffix})`;
|
|
18142
|
+
}
|
|
18143
|
+
async function applyStructuredAssertionsToResult(input) {
|
|
18144
|
+
const assertions = input.scenario.assertions ?? [];
|
|
18145
|
+
if (assertions.length === 0) {
|
|
18146
|
+
return {
|
|
18147
|
+
status: input.status,
|
|
18148
|
+
reasoning: input.reasoning,
|
|
18149
|
+
assertionsPassed: [],
|
|
18150
|
+
assertionsFailed: [],
|
|
18151
|
+
assertionResults: []
|
|
18152
|
+
};
|
|
18153
|
+
}
|
|
18154
|
+
const results = await evaluateAssertions(input.page, assertions, {
|
|
18155
|
+
consoleErrors: input.consoleErrors
|
|
18156
|
+
});
|
|
18157
|
+
const assertionsPassed = results.filter((r) => r.passed).map(summarizeAssertionResult);
|
|
18158
|
+
const assertionsFailed = results.filter((r) => !r.passed).map(summarizeAssertionResult);
|
|
18159
|
+
const assertionResults = results.map((result) => ({
|
|
18160
|
+
type: result.assertion.type,
|
|
18161
|
+
description: assertionDescription(result),
|
|
18162
|
+
passed: result.passed,
|
|
18163
|
+
actual: result.actual,
|
|
18164
|
+
...result.error ? { error: result.error } : {}
|
|
18165
|
+
}));
|
|
18166
|
+
const assertionsOk = allAssertionsPassed(results);
|
|
18167
|
+
const status = assertionsOk || input.status !== "passed" ? input.status : "failed";
|
|
18168
|
+
const assertionHeading = assertionsOk ? "Structured assertions passed:" : "Structured assertions failed:";
|
|
18169
|
+
const reasoningParts = [input.reasoning, `${assertionHeading}
|
|
18170
|
+
${formatAssertionResults(results)}`].map((part) => part.trim()).filter(Boolean);
|
|
18171
|
+
return {
|
|
18172
|
+
status,
|
|
18173
|
+
reasoning: reasoningParts.join(`
|
|
18174
|
+
|
|
18175
|
+
`),
|
|
18176
|
+
assertionsPassed,
|
|
18177
|
+
assertionsFailed,
|
|
18178
|
+
assertionResults
|
|
18179
|
+
};
|
|
18180
|
+
}
|
|
17667
18181
|
function withTimeout(promise, ms, label) {
|
|
17668
18182
|
return new Promise((resolve, reject) => {
|
|
17669
18183
|
const warningAt = Math.floor(ms * 0.8);
|
|
@@ -17834,6 +18348,7 @@ async function runSingleScenario(scenario, runId, options) {
|
|
|
17834
18348
|
model,
|
|
17835
18349
|
runId,
|
|
17836
18350
|
sessionId: result.id,
|
|
18351
|
+
baseUrl: options.url,
|
|
17837
18352
|
maxTurns: effectiveOptions.minimal ? 10 : 30,
|
|
17838
18353
|
a11y: effectiveOptions.a11y,
|
|
17839
18354
|
persona: persona ? {
|
|
@@ -17916,27 +18431,46 @@ async function runSingleScenario(scenario, runId, options) {
|
|
|
17916
18431
|
closeSession(result.id);
|
|
17917
18432
|
const lightpandaNote = options.engine === "lightpanda" ? " (Running with Lightpanda \u2014 no screenshots)" : options.engine === "bun" ? " (Running with Bun.WebView \u2014 native, ~11x faster)" : "";
|
|
17918
18433
|
const networkMeta = networkErrors.length > 0 ? { networkErrors: networkErrors.slice(0, 20) } : {};
|
|
17919
|
-
|
|
18434
|
+
const baseReasoning = agentResult.reasoning ? agentResult.reasoning + lightpandaNote : lightpandaNote || "";
|
|
18435
|
+
const assertionOutcome = await applyStructuredAssertionsToResult({
|
|
18436
|
+
page,
|
|
18437
|
+
scenario,
|
|
18438
|
+
consoleErrors,
|
|
17920
18439
|
status: agentResult.status,
|
|
17921
|
-
reasoning:
|
|
18440
|
+
reasoning: baseReasoning
|
|
18441
|
+
});
|
|
18442
|
+
const structuredAssertionMeta = assertionOutcome.assertionResults.length > 0 ? {
|
|
18443
|
+
structuredAssertions: {
|
|
18444
|
+
passed: assertionOutcome.assertionsPassed,
|
|
18445
|
+
failed: assertionOutcome.assertionsFailed,
|
|
18446
|
+
results: assertionOutcome.assertionResults
|
|
18447
|
+
}
|
|
18448
|
+
} : {};
|
|
18449
|
+
let updatedResult = updateResult(result.id, {
|
|
18450
|
+
status: assertionOutcome.status,
|
|
18451
|
+
reasoning: assertionOutcome.reasoning || undefined,
|
|
17922
18452
|
stepsCompleted: agentResult.stepsCompleted,
|
|
17923
18453
|
durationMs: Date.now() - new Date(result.createdAt).getTime(),
|
|
17924
18454
|
tokensUsed: agentResult.tokensUsed,
|
|
17925
18455
|
costCents: estimateCost(model, agentResult.tokensUsed),
|
|
17926
|
-
metadata: {
|
|
18456
|
+
metadata: {
|
|
18457
|
+
consoleLogs,
|
|
18458
|
+
...networkErrors.length > 0 ? networkMeta : {},
|
|
18459
|
+
...structuredAssertionMeta
|
|
18460
|
+
}
|
|
17927
18461
|
});
|
|
17928
|
-
if (
|
|
17929
|
-
const failureAnalysis = analyzeFailure(null,
|
|
18462
|
+
if (assertionOutcome.status === "failed" || assertionOutcome.status === "error") {
|
|
18463
|
+
const failureAnalysis = analyzeFailure(null, assertionOutcome.reasoning ?? null);
|
|
17930
18464
|
if (failureAnalysis) {
|
|
17931
18465
|
updatedResult = updateResult(result.id, { failureAnalysis });
|
|
17932
18466
|
}
|
|
17933
18467
|
}
|
|
17934
|
-
if (
|
|
18468
|
+
if (assertionOutcome.status === "passed") {
|
|
17935
18469
|
try {
|
|
17936
18470
|
updateScenarioPassedCache(scenario.id, options.url);
|
|
17937
18471
|
} catch {}
|
|
17938
18472
|
}
|
|
17939
|
-
const eventType =
|
|
18473
|
+
const eventType = assertionOutcome.status === "passed" ? "scenario:pass" : "scenario:fail";
|
|
17940
18474
|
emit({ type: eventType, scenarioId: scenario.id, scenarioName: scenario.name, resultId: result.id, runId });
|
|
17941
18475
|
return updatedResult;
|
|
17942
18476
|
} catch (error) {
|
|
@@ -17961,7 +18495,8 @@ async function runSingleScenario(scenario, runId, options) {
|
|
|
17961
18495
|
} finally {
|
|
17962
18496
|
if (harPath) {
|
|
17963
18497
|
try {
|
|
17964
|
-
|
|
18498
|
+
const existing = getResult(result.id);
|
|
18499
|
+
updateResult(result.id, { metadata: { ...existing?.metadata ?? {}, harPath } });
|
|
17965
18500
|
} catch {}
|
|
17966
18501
|
}
|
|
17967
18502
|
if (browser) {
|
|
@@ -18133,22 +18668,31 @@ async function runBatch(scenarios, options) {
|
|
|
18133
18668
|
}
|
|
18134
18669
|
return { run: finalRun, results };
|
|
18135
18670
|
}
|
|
18136
|
-
|
|
18137
|
-
|
|
18671
|
+
function findScenarioInList(scenarios, id) {
|
|
18672
|
+
return scenarios.find((scenario) => scenario.id === id || scenario.shortId === id || scenario.id.startsWith(id)) ?? null;
|
|
18673
|
+
}
|
|
18674
|
+
function resolveScenariosForRun(options) {
|
|
18138
18675
|
if (options.scenarioIds && options.scenarioIds.length > 0) {
|
|
18139
|
-
const
|
|
18140
|
-
|
|
18141
|
-
|
|
18142
|
-
|
|
18143
|
-
|
|
18676
|
+
const scoped = listScenarios({ projectId: options.projectId });
|
|
18677
|
+
const resolved = [];
|
|
18678
|
+
const seen = new Set;
|
|
18679
|
+
for (const id of options.scenarioIds) {
|
|
18680
|
+
const scenario = findScenarioInList(scoped, id) ?? getScenario(id);
|
|
18681
|
+
if (scenario && !seen.has(scenario.id)) {
|
|
18682
|
+
resolved.push(scenario);
|
|
18683
|
+
seen.add(scenario.id);
|
|
18684
|
+
}
|
|
18144
18685
|
}
|
|
18145
|
-
|
|
18146
|
-
scenarios = listScenarios({
|
|
18147
|
-
projectId: options.projectId,
|
|
18148
|
-
tags: options.tags,
|
|
18149
|
-
priority: options.priority
|
|
18150
|
-
});
|
|
18686
|
+
return resolved;
|
|
18151
18687
|
}
|
|
18688
|
+
return listScenarios({
|
|
18689
|
+
projectId: options.projectId,
|
|
18690
|
+
tags: options.tags,
|
|
18691
|
+
priority: options.priority
|
|
18692
|
+
});
|
|
18693
|
+
}
|
|
18694
|
+
async function runByFilter(options) {
|
|
18695
|
+
const scenarios = resolveScenariosForRun(options);
|
|
18152
18696
|
if (scenarios.length === 0) {
|
|
18153
18697
|
const config = loadConfig();
|
|
18154
18698
|
const model = resolveModel(options.model ?? config.defaultModel);
|
|
@@ -18161,17 +18705,7 @@ async function runByFilter(options) {
|
|
|
18161
18705
|
function startRunAsync(options) {
|
|
18162
18706
|
const config = loadConfig();
|
|
18163
18707
|
const model = resolveModel(options.model ?? config.defaultModel);
|
|
18164
|
-
|
|
18165
|
-
if (options.scenarioIds && options.scenarioIds.length > 0) {
|
|
18166
|
-
const all = listScenarios({ projectId: options.projectId });
|
|
18167
|
-
scenarios = all.filter((s) => options.scenarioIds.includes(s.id) || options.scenarioIds.includes(s.shortId));
|
|
18168
|
-
} else {
|
|
18169
|
-
scenarios = listScenarios({
|
|
18170
|
-
projectId: options.projectId,
|
|
18171
|
-
tags: options.tags,
|
|
18172
|
-
priority: options.priority
|
|
18173
|
-
});
|
|
18174
|
-
}
|
|
18708
|
+
const scenarios = resolveScenariosForRun(options);
|
|
18175
18709
|
if (!options.skipBudgetCheck) {
|
|
18176
18710
|
const cap = options.maxCostCents ?? config.defaultMaxCostCents;
|
|
18177
18711
|
if (cap !== undefined && cap > 0 && scenarios.length > 0) {
|
|
@@ -18275,6 +18809,7 @@ var init_runner = __esm(() => {
|
|
|
18275
18809
|
init_session_tracker();
|
|
18276
18810
|
init_webhooks();
|
|
18277
18811
|
init_failure_pipeline();
|
|
18812
|
+
init_assertions();
|
|
18278
18813
|
});
|
|
18279
18814
|
|
|
18280
18815
|
// src/lib/reporter.ts
|
|
@@ -25414,18 +25949,7 @@ function normalizeFilter(input) {
|
|
|
25414
25949
|
};
|
|
25415
25950
|
}
|
|
25416
25951
|
function normalizeExecution(input) {
|
|
25417
|
-
|
|
25418
|
-
if (target === "connector:e2b") {
|
|
25419
|
-
return {
|
|
25420
|
-
target,
|
|
25421
|
-
connector: input?.connector ?? "e2b",
|
|
25422
|
-
operation: input?.operation ?? "run",
|
|
25423
|
-
sandboxTemplate: input?.sandboxTemplate,
|
|
25424
|
-
timeoutMs: input?.timeoutMs,
|
|
25425
|
-
env: input?.env
|
|
25426
|
-
};
|
|
25427
|
-
}
|
|
25428
|
-
return { ...DEFAULT_EXECUTION, timeoutMs: input?.timeoutMs };
|
|
25952
|
+
return input ? workflowExecutionFromValue(input) : DEFAULT_EXECUTION;
|
|
25429
25953
|
}
|
|
25430
25954
|
function createTestingWorkflow(input) {
|
|
25431
25955
|
const db2 = getDatabase();
|
|
@@ -26029,11 +26553,11 @@ import { existsSync as existsSync42, writeFileSync as writeFileSync32, readFileS
|
|
|
26029
26553
|
import { join as join42 } from "path";
|
|
26030
26554
|
import { Database as Database4 } from "bun:sqlite";
|
|
26031
26555
|
import { existsSync as existsSync16, mkdirSync as mkdirSync13 } from "fs";
|
|
26032
|
-
import { dirname as dirname4, join as
|
|
26033
|
-
import { existsSync as existsSync22, writeFileSync as
|
|
26556
|
+
import { dirname as dirname4, join as join19, resolve as resolve2 } from "path";
|
|
26557
|
+
import { existsSync as existsSync22, writeFileSync as writeFileSync7 } from "fs";
|
|
26034
26558
|
import { join as join22 } from "path";
|
|
26035
26559
|
import { execSync as execSync3, execFileSync } from "child_process";
|
|
26036
|
-
import { existsSync as existsSync32, readFileSync as
|
|
26560
|
+
import { existsSync as existsSync32, readFileSync as readFileSync7, writeFileSync as writeFileSync22, mkdirSync as mkdirSync22 } from "fs";
|
|
26037
26561
|
import { homedir as homedir11 } from "os";
|
|
26038
26562
|
import { join as join32, dirname as dirname22 } from "path";
|
|
26039
26563
|
import { hostname } from "os";
|
|
@@ -26204,12 +26728,12 @@ function getDbPath2() {
|
|
|
26204
26728
|
return process.env["PROJECTS_DB_PATH"];
|
|
26205
26729
|
}
|
|
26206
26730
|
const home = process.env["HOME"] || process.env["USERPROFILE"] || "~";
|
|
26207
|
-
return
|
|
26731
|
+
return join19(home, ".hasna", "projects", "projects.db");
|
|
26208
26732
|
}
|
|
26209
26733
|
function ensureDir2(filePath) {
|
|
26210
26734
|
if (filePath === ":memory:")
|
|
26211
26735
|
return;
|
|
26212
|
-
const dir = dirname4(
|
|
26736
|
+
const dir = dirname4(resolve2(filePath));
|
|
26213
26737
|
if (!existsSync16(dir)) {
|
|
26214
26738
|
mkdirSync13(dir, { recursive: true });
|
|
26215
26739
|
}
|
|
@@ -26249,7 +26773,7 @@ function gitInit(project) {
|
|
|
26249
26773
|
execSync3("git init", { cwd: path, stdio: "pipe" });
|
|
26250
26774
|
const gitignorePath = join22(path, ".gitignore");
|
|
26251
26775
|
if (!existsSync22(gitignorePath)) {
|
|
26252
|
-
|
|
26776
|
+
writeFileSync7(gitignorePath, GITIGNORE_TEMPLATE, "utf-8");
|
|
26253
26777
|
}
|
|
26254
26778
|
const projectJson = {
|
|
26255
26779
|
id,
|
|
@@ -26258,7 +26782,7 @@ function gitInit(project) {
|
|
|
26258
26782
|
created_at: project.created_at,
|
|
26259
26783
|
integrations: project.integrations ?? {}
|
|
26260
26784
|
};
|
|
26261
|
-
|
|
26785
|
+
writeFileSync7(join22(path, ".project.json"), JSON.stringify(projectJson, null, 2) + `
|
|
26262
26786
|
`, "utf-8");
|
|
26263
26787
|
execSync3("git add .gitignore .project.json", { cwd: path, stdio: "pipe" });
|
|
26264
26788
|
execSync3(`git commit -m "chore: init project ${name}"`, {
|
|
@@ -26270,7 +26794,7 @@ function gitInit(project) {
|
|
|
26270
26794
|
function getConfig() {
|
|
26271
26795
|
if (existsSync32(CONFIG_PATH3)) {
|
|
26272
26796
|
try {
|
|
26273
|
-
const user = JSON.parse(
|
|
26797
|
+
const user = JSON.parse(readFileSync7(CONFIG_PATH3, "utf-8"));
|
|
26274
26798
|
return { ...DEFAULTS, ...user };
|
|
26275
26799
|
} catch {
|
|
26276
26800
|
return { ...DEFAULTS };
|
|
@@ -42345,11 +42869,11 @@ More information can be found at: https://a.co/c895JFp`);
|
|
|
42345
42869
|
var numberSelector = (obj, key, type2) => {
|
|
42346
42870
|
if (!(key in obj))
|
|
42347
42871
|
return;
|
|
42348
|
-
const
|
|
42349
|
-
if (Number.isNaN(
|
|
42872
|
+
const numberValue2 = parseInt(obj[key], 10);
|
|
42873
|
+
if (Number.isNaN(numberValue2)) {
|
|
42350
42874
|
throw new TypeError(`Cannot load ${type2} '${key}'. Expected number, got '${obj[key]}'.`);
|
|
42351
42875
|
}
|
|
42352
|
-
return
|
|
42876
|
+
return numberValue2;
|
|
42353
42877
|
};
|
|
42354
42878
|
exports.SelectorType = undefined;
|
|
42355
42879
|
(function(SelectorType2) {
|
|
@@ -57095,7 +57619,7 @@ __export(exports_openapi_import, {
|
|
|
57095
57619
|
importFromOpenAPI: () => importFromOpenAPI,
|
|
57096
57620
|
importApiChecksFromOpenAPI: () => importApiChecksFromOpenAPI
|
|
57097
57621
|
});
|
|
57098
|
-
import { readFileSync as
|
|
57622
|
+
import { readFileSync as readFileSync8 } from "fs";
|
|
57099
57623
|
function parseSpec(content) {
|
|
57100
57624
|
try {
|
|
57101
57625
|
return JSON.parse(content);
|
|
@@ -57124,7 +57648,7 @@ function parseOpenAPISpec(filePathOrUrl) {
|
|
|
57124
57648
|
if (filePathOrUrl.startsWith("http")) {
|
|
57125
57649
|
throw new Error("URL fetching not supported yet. Download the spec file first.");
|
|
57126
57650
|
}
|
|
57127
|
-
content =
|
|
57651
|
+
content = readFileSync8(filePathOrUrl, "utf-8");
|
|
57128
57652
|
const spec = parseSpec(content);
|
|
57129
57653
|
const isOpenAPI3 = !!spec.openapi;
|
|
57130
57654
|
const isSwagger2 = !!spec.swagger;
|
|
@@ -57187,7 +57711,7 @@ function parseOpenAPISpecAsChecks(filePathOrUrl) {
|
|
|
57187
57711
|
if (filePathOrUrl.startsWith("http")) {
|
|
57188
57712
|
throw new Error("URL fetching not supported yet. Download the spec file first.");
|
|
57189
57713
|
}
|
|
57190
|
-
content =
|
|
57714
|
+
content = readFileSync8(filePathOrUrl, "utf-8");
|
|
57191
57715
|
const spec = parseSpec(content);
|
|
57192
57716
|
if (!spec.openapi && !spec.swagger) {
|
|
57193
57717
|
throw new Error("Not a valid OpenAPI 3.x or Swagger 2.0 spec");
|
|
@@ -57527,7 +58051,7 @@ async function recordSession(url, options) {
|
|
|
57527
58051
|
await Promise.race([
|
|
57528
58052
|
page.waitForEvent("close").catch(() => {}),
|
|
57529
58053
|
context.waitForEvent("close").catch(() => {}),
|
|
57530
|
-
new Promise((
|
|
58054
|
+
new Promise((resolve3) => setTimeout(resolve3, timeout))
|
|
57531
58055
|
]);
|
|
57532
58056
|
clearInterval(pollInterval);
|
|
57533
58057
|
try {
|
|
@@ -75898,7 +76422,7 @@ function createProviderToolFactoryWithOutputSchema({
|
|
|
75898
76422
|
supportsDeferredResults
|
|
75899
76423
|
});
|
|
75900
76424
|
}
|
|
75901
|
-
async function
|
|
76425
|
+
async function resolve3(value) {
|
|
75902
76426
|
if (typeof value === "function") {
|
|
75903
76427
|
value = value();
|
|
75904
76428
|
}
|
|
@@ -77585,7 +78109,7 @@ var import_oidc, import_oidc2, marker17 = "vercel.ai.gateway.error", symbol18, _
|
|
|
77585
78109
|
try {
|
|
77586
78110
|
const { value } = await getFromApi({
|
|
77587
78111
|
url: `${this.config.baseURL}/config`,
|
|
77588
|
-
headers: await
|
|
78112
|
+
headers: await resolve3(this.config.headers()),
|
|
77589
78113
|
successfulResponseHandler: createJsonResponseHandler(gatewayAvailableModelsResponseSchema),
|
|
77590
78114
|
failedResponseHandler: createJsonErrorResponseHandler({
|
|
77591
78115
|
errorSchema: exports_external2.any(),
|
|
@@ -77603,7 +78127,7 @@ var import_oidc, import_oidc2, marker17 = "vercel.ai.gateway.error", symbol18, _
|
|
|
77603
78127
|
const baseUrl = new URL(this.config.baseURL);
|
|
77604
78128
|
const { value } = await getFromApi({
|
|
77605
78129
|
url: `${baseUrl.origin}/v1/credits`,
|
|
77606
|
-
headers: await
|
|
78130
|
+
headers: await resolve3(this.config.headers()),
|
|
77607
78131
|
successfulResponseHandler: createJsonResponseHandler(gatewayCreditsResponseSchema),
|
|
77608
78132
|
failedResponseHandler: createJsonErrorResponseHandler({
|
|
77609
78133
|
errorSchema: exports_external2.any(),
|
|
@@ -77649,7 +78173,7 @@ var import_oidc, import_oidc2, marker17 = "vercel.ai.gateway.error", symbol18, _
|
|
|
77649
78173
|
}
|
|
77650
78174
|
const { value } = await getFromApi({
|
|
77651
78175
|
url: `${baseUrl.origin}/v1/report?${searchParams.toString()}`,
|
|
77652
|
-
headers: await
|
|
78176
|
+
headers: await resolve3(this.config.headers()),
|
|
77653
78177
|
successfulResponseHandler: createJsonResponseHandler(gatewaySpendReportResponseSchema),
|
|
77654
78178
|
failedResponseHandler: createJsonErrorResponseHandler({
|
|
77655
78179
|
errorSchema: exports_external2.any(),
|
|
@@ -77671,7 +78195,7 @@ var import_oidc, import_oidc2, marker17 = "vercel.ai.gateway.error", symbol18, _
|
|
|
77671
78195
|
const baseUrl = new URL(this.config.baseURL);
|
|
77672
78196
|
const { value } = await getFromApi({
|
|
77673
78197
|
url: `${baseUrl.origin}/v1/generation?id=${encodeURIComponent(params.id)}`,
|
|
77674
|
-
headers: await
|
|
78198
|
+
headers: await resolve3(this.config.headers()),
|
|
77675
78199
|
successfulResponseHandler: createJsonResponseHandler(gatewayGenerationInfoResponseSchema),
|
|
77676
78200
|
failedResponseHandler: createJsonErrorResponseHandler({
|
|
77677
78201
|
errorSchema: exports_external2.any(),
|
|
@@ -77704,7 +78228,7 @@ var import_oidc, import_oidc2, marker17 = "vercel.ai.gateway.error", symbol18, _
|
|
|
77704
78228
|
async doGenerate(options) {
|
|
77705
78229
|
const { args, warnings } = await this.getArgs(options);
|
|
77706
78230
|
const { abortSignal } = options;
|
|
77707
|
-
const resolvedHeaders = await
|
|
78231
|
+
const resolvedHeaders = await resolve3(this.config.headers());
|
|
77708
78232
|
try {
|
|
77709
78233
|
const {
|
|
77710
78234
|
responseHeaders,
|
|
@@ -77712,7 +78236,7 @@ var import_oidc, import_oidc2, marker17 = "vercel.ai.gateway.error", symbol18, _
|
|
|
77712
78236
|
rawValue: rawResponse
|
|
77713
78237
|
} = await postJsonToApi({
|
|
77714
78238
|
url: this.getUrl(),
|
|
77715
|
-
headers: combineHeaders(resolvedHeaders, options.headers, this.getModelConfigHeaders(this.modelId, false), await
|
|
78239
|
+
headers: combineHeaders(resolvedHeaders, options.headers, this.getModelConfigHeaders(this.modelId, false), await resolve3(this.config.o11yHeaders)),
|
|
77716
78240
|
body: args,
|
|
77717
78241
|
successfulResponseHandler: createJsonResponseHandler(exports_external2.any()),
|
|
77718
78242
|
failedResponseHandler: createJsonErrorResponseHandler({
|
|
@@ -77735,11 +78259,11 @@ var import_oidc, import_oidc2, marker17 = "vercel.ai.gateway.error", symbol18, _
|
|
|
77735
78259
|
async doStream(options) {
|
|
77736
78260
|
const { args, warnings } = await this.getArgs(options);
|
|
77737
78261
|
const { abortSignal } = options;
|
|
77738
|
-
const resolvedHeaders = await
|
|
78262
|
+
const resolvedHeaders = await resolve3(this.config.headers());
|
|
77739
78263
|
try {
|
|
77740
78264
|
const { value: response, responseHeaders } = await postJsonToApi({
|
|
77741
78265
|
url: this.getUrl(),
|
|
77742
|
-
headers: combineHeaders(resolvedHeaders, options.headers, this.getModelConfigHeaders(this.modelId, true), await
|
|
78266
|
+
headers: combineHeaders(resolvedHeaders, options.headers, this.getModelConfigHeaders(this.modelId, true), await resolve3(this.config.o11yHeaders)),
|
|
77743
78267
|
body: args,
|
|
77744
78268
|
successfulResponseHandler: createEventSourceResponseHandler(exports_external2.any()),
|
|
77745
78269
|
failedResponseHandler: createJsonErrorResponseHandler({
|
|
@@ -77824,7 +78348,7 @@ var import_oidc, import_oidc2, marker17 = "vercel.ai.gateway.error", symbol18, _
|
|
|
77824
78348
|
providerOptions
|
|
77825
78349
|
}) {
|
|
77826
78350
|
var _a92;
|
|
77827
|
-
const resolvedHeaders = await
|
|
78351
|
+
const resolvedHeaders = await resolve3(this.config.headers());
|
|
77828
78352
|
try {
|
|
77829
78353
|
const {
|
|
77830
78354
|
responseHeaders,
|
|
@@ -77832,7 +78356,7 @@ var import_oidc, import_oidc2, marker17 = "vercel.ai.gateway.error", symbol18, _
|
|
|
77832
78356
|
rawValue
|
|
77833
78357
|
} = await postJsonToApi({
|
|
77834
78358
|
url: this.getUrl(),
|
|
77835
|
-
headers: combineHeaders(resolvedHeaders, headers != null ? headers : {}, this.getModelConfigHeaders(), await
|
|
78359
|
+
headers: combineHeaders(resolvedHeaders, headers != null ? headers : {}, this.getModelConfigHeaders(), await resolve3(this.config.o11yHeaders)),
|
|
77836
78360
|
body: {
|
|
77837
78361
|
values,
|
|
77838
78362
|
...providerOptions ? { providerOptions } : {}
|
|
@@ -77888,7 +78412,7 @@ var import_oidc, import_oidc2, marker17 = "vercel.ai.gateway.error", symbol18, _
|
|
|
77888
78412
|
abortSignal
|
|
77889
78413
|
}) {
|
|
77890
78414
|
var _a92, _b92, _c2, _d2;
|
|
77891
|
-
const resolvedHeaders = await
|
|
78415
|
+
const resolvedHeaders = await resolve3(this.config.headers());
|
|
77892
78416
|
try {
|
|
77893
78417
|
const {
|
|
77894
78418
|
responseHeaders,
|
|
@@ -77896,7 +78420,7 @@ var import_oidc, import_oidc2, marker17 = "vercel.ai.gateway.error", symbol18, _
|
|
|
77896
78420
|
rawValue
|
|
77897
78421
|
} = await postJsonToApi({
|
|
77898
78422
|
url: this.getUrl(),
|
|
77899
|
-
headers: combineHeaders(resolvedHeaders, headers != null ? headers : {}, this.getModelConfigHeaders(), await
|
|
78423
|
+
headers: combineHeaders(resolvedHeaders, headers != null ? headers : {}, this.getModelConfigHeaders(), await resolve3(this.config.o11yHeaders)),
|
|
77900
78424
|
body: {
|
|
77901
78425
|
prompt,
|
|
77902
78426
|
n: n2,
|
|
@@ -77971,11 +78495,11 @@ var import_oidc, import_oidc2, marker17 = "vercel.ai.gateway.error", symbol18, _
|
|
|
77971
78495
|
abortSignal
|
|
77972
78496
|
}) {
|
|
77973
78497
|
var _a92;
|
|
77974
|
-
const resolvedHeaders = await
|
|
78498
|
+
const resolvedHeaders = await resolve3(this.config.headers());
|
|
77975
78499
|
try {
|
|
77976
78500
|
const { responseHeaders, value: responseBody } = await postJsonToApi({
|
|
77977
78501
|
url: this.getUrl(),
|
|
77978
|
-
headers: combineHeaders(resolvedHeaders, headers != null ? headers : {}, this.getModelConfigHeaders(), await
|
|
78502
|
+
headers: combineHeaders(resolvedHeaders, headers != null ? headers : {}, this.getModelConfigHeaders(), await resolve3(this.config.o11yHeaders), { accept: "text/event-stream" }),
|
|
77979
78503
|
body: {
|
|
77980
78504
|
prompt,
|
|
77981
78505
|
n: n2,
|
|
@@ -78098,7 +78622,7 @@ var import_oidc, import_oidc2, marker17 = "vercel.ai.gateway.error", symbol18, _
|
|
|
78098
78622
|
abortSignal,
|
|
78099
78623
|
providerOptions
|
|
78100
78624
|
}) {
|
|
78101
|
-
const resolvedHeaders = await
|
|
78625
|
+
const resolvedHeaders = await resolve3(this.config.headers());
|
|
78102
78626
|
try {
|
|
78103
78627
|
const {
|
|
78104
78628
|
responseHeaders,
|
|
@@ -78106,7 +78630,7 @@ var import_oidc, import_oidc2, marker17 = "vercel.ai.gateway.error", symbol18, _
|
|
|
78106
78630
|
rawValue
|
|
78107
78631
|
} = await postJsonToApi({
|
|
78108
78632
|
url: this.getUrl(),
|
|
78109
|
-
headers: combineHeaders(resolvedHeaders, headers != null ? headers : {}, this.getModelConfigHeaders(), await
|
|
78633
|
+
headers: combineHeaders(resolvedHeaders, headers != null ? headers : {}, this.getModelConfigHeaders(), await resolve3(this.config.o11yHeaders)),
|
|
78110
78634
|
body: {
|
|
78111
78635
|
documents,
|
|
78112
78636
|
query,
|
|
@@ -87524,7 +88048,7 @@ var import_api2, import_api3, __defProp4, __export4 = (target, all) => {
|
|
|
87524
88048
|
const schema = asSchema(inputSchema);
|
|
87525
88049
|
return {
|
|
87526
88050
|
name: "object",
|
|
87527
|
-
responseFormat:
|
|
88051
|
+
responseFormat: resolve3(schema.jsonSchema).then((jsonSchema2) => ({
|
|
87528
88052
|
type: "json",
|
|
87529
88053
|
schema: jsonSchema2,
|
|
87530
88054
|
...name21 != null && { name: name21 },
|
|
@@ -87585,7 +88109,7 @@ var import_api2, import_api3, __defProp4, __export4 = (target, all) => {
|
|
|
87585
88109
|
const elementSchema = asSchema(inputElementSchema);
|
|
87586
88110
|
return {
|
|
87587
88111
|
name: "array",
|
|
87588
|
-
responseFormat:
|
|
88112
|
+
responseFormat: resolve3(elementSchema.jsonSchema).then((jsonSchema2) => {
|
|
87589
88113
|
const { $schema, ...itemSchema } = jsonSchema2;
|
|
87590
88114
|
return {
|
|
87591
88115
|
type: "json",
|
|
@@ -90482,9 +91006,9 @@ var import_api2, import_api3, __defProp4, __export4 = (target, all) => {
|
|
|
90482
91006
|
...options
|
|
90483
91007
|
}) {
|
|
90484
91008
|
var _a21, _b16, _c2, _d2, _e2;
|
|
90485
|
-
const resolvedBody = await
|
|
90486
|
-
const resolvedHeaders = await
|
|
90487
|
-
const resolvedCredentials = await
|
|
91009
|
+
const resolvedBody = await resolve3(this.body);
|
|
91010
|
+
const resolvedHeaders = await resolve3(this.headers);
|
|
91011
|
+
const resolvedCredentials = await resolve3(this.credentials);
|
|
90488
91012
|
const baseHeaders = {
|
|
90489
91013
|
...normalizeHeaders(resolvedHeaders),
|
|
90490
91014
|
...normalizeHeaders(options.headers)
|
|
@@ -90532,9 +91056,9 @@ var import_api2, import_api3, __defProp4, __export4 = (target, all) => {
|
|
|
90532
91056
|
}
|
|
90533
91057
|
async reconnectToStream(options) {
|
|
90534
91058
|
var _a21, _b16, _c2, _d2, _e2;
|
|
90535
|
-
const resolvedBody = await
|
|
90536
|
-
const resolvedHeaders = await
|
|
90537
|
-
const resolvedCredentials = await
|
|
91059
|
+
const resolvedBody = await resolve3(this.body);
|
|
91060
|
+
const resolvedHeaders = await resolve3(this.headers);
|
|
91061
|
+
const resolvedCredentials = await resolve3(this.credentials);
|
|
90538
91062
|
const baseHeaders = {
|
|
90539
91063
|
...normalizeHeaders(resolvedHeaders),
|
|
90540
91064
|
...normalizeHeaders(options.headers)
|
|
@@ -92755,7 +93279,7 @@ __export(exports_session_converter, {
|
|
|
92755
93279
|
convertSessionToScenario: () => convertSessionToScenario,
|
|
92756
93280
|
convertSessionFile: () => convertSessionFile
|
|
92757
93281
|
});
|
|
92758
|
-
import { readFileSync as
|
|
93282
|
+
import { readFileSync as readFileSync9 } from "fs";
|
|
92759
93283
|
import { extname } from "path";
|
|
92760
93284
|
function parseRrwebSession(events) {
|
|
92761
93285
|
const result = [];
|
|
@@ -92941,7 +93465,7 @@ ${condensed}`;
|
|
|
92941
93465
|
};
|
|
92942
93466
|
}
|
|
92943
93467
|
async function convertSessionFile(filePath, format, options) {
|
|
92944
|
-
const raw =
|
|
93468
|
+
const raw = readFileSync9(filePath, "utf-8");
|
|
92945
93469
|
let parsed;
|
|
92946
93470
|
try {
|
|
92947
93471
|
parsed = JSON.parse(raw);
|
|
@@ -92975,7 +93499,7 @@ function detectSessionFormat(filePath) {
|
|
|
92975
93499
|
if (ext === ".har")
|
|
92976
93500
|
return "har";
|
|
92977
93501
|
try {
|
|
92978
|
-
const content =
|
|
93502
|
+
const content = readFileSync9(filePath, "utf-8").trim();
|
|
92979
93503
|
const parsed = JSON.parse(content);
|
|
92980
93504
|
if (Array.isArray(parsed) && parsed[0]?.type !== undefined && typeof parsed[0]?.timestamp === "number") {
|
|
92981
93505
|
return "rrweb";
|
|
@@ -93419,7 +93943,7 @@ import chalk6 from "chalk";
|
|
|
93419
93943
|
// package.json
|
|
93420
93944
|
var package_default = {
|
|
93421
93945
|
name: "@hasna/testers",
|
|
93422
|
-
version: "0.0.
|
|
93946
|
+
version: "0.0.35",
|
|
93423
93947
|
description: "AI-powered QA testing CLI \u2014 spawns cheap AI agents to test web apps with headless browsers",
|
|
93424
93948
|
type: "module",
|
|
93425
93949
|
main: "dist/index.js",
|
|
@@ -93443,10 +93967,10 @@ var package_default = {
|
|
|
93443
93967
|
],
|
|
93444
93968
|
scripts: {
|
|
93445
93969
|
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",
|
|
93446
|
-
"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",
|
|
93447
|
-
"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",
|
|
93448
|
-
"build:server": "bun build src/server/index.ts --outdir dist/server --target bun --external @anthropic-ai/sdk --external playwright --external @hasna/browser",
|
|
93449
|
-
"build:lib": "bun build src/index.ts --outdir dist --target bun --external playwright --external @anthropic-ai/sdk --external @modelcontextprotocol/sdk --external @hasna/browser",
|
|
93970
|
+
"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",
|
|
93971
|
+
"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",
|
|
93972
|
+
"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",
|
|
93973
|
+
"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",
|
|
93450
93974
|
"build:types": "NODE_OPTIONS='--max-old-space-size=8192' tsc --emitDeclarationOnly --outDir dist --skipLibCheck || true",
|
|
93451
93975
|
"build:dashboard": "cd dashboard && bun run build",
|
|
93452
93976
|
"build:ext": "cd extension && bun run build",
|
|
@@ -93460,10 +93984,11 @@ var package_default = {
|
|
|
93460
93984
|
},
|
|
93461
93985
|
dependencies: {
|
|
93462
93986
|
"@anthropic-ai/sdk": "^0.52.0",
|
|
93463
|
-
"@hasna/browser": "^0.4.
|
|
93987
|
+
"@hasna/browser": "^0.4.12",
|
|
93464
93988
|
"@hasna/cloud": "^0.1.24",
|
|
93465
93989
|
"@hasna/contacts": "^0.6.8",
|
|
93466
93990
|
"@hasna/projects": "^0.1.42",
|
|
93991
|
+
"@hasna/sandboxes": "^0.1.27",
|
|
93467
93992
|
"@modelcontextprotocol/sdk": "^1.12.1",
|
|
93468
93993
|
ai: "^6.0.175",
|
|
93469
93994
|
chalk: "^5.4.1",
|
|
@@ -93516,9 +94041,9 @@ init_todos_connector();
|
|
|
93516
94041
|
init_browser();
|
|
93517
94042
|
import { render, Box, Text, useInput, useApp } from "ink";
|
|
93518
94043
|
import React, { useState } from "react";
|
|
93519
|
-
import { readFileSync as
|
|
94044
|
+
import { readFileSync as readFileSync10, readdirSync as readdirSync6, writeFileSync as writeFileSync8 } from "fs";
|
|
93520
94045
|
import { createInterface } from "readline";
|
|
93521
|
-
import { join as
|
|
94046
|
+
import { join as join20, resolve as resolve4 } from "path";
|
|
93522
94047
|
|
|
93523
94048
|
// src/lib/init.ts
|
|
93524
94049
|
init_paths();
|
|
@@ -95247,9 +95772,13 @@ init_flows();
|
|
|
95247
95772
|
init_workflows();
|
|
95248
95773
|
|
|
95249
95774
|
// src/lib/workflow-runner.ts
|
|
95775
|
+
init_database();
|
|
95250
95776
|
init_workflows();
|
|
95251
95777
|
init_personas();
|
|
95252
95778
|
init_runner();
|
|
95779
|
+
import { mkdtempSync, rmSync, writeFileSync as writeFileSync4 } from "fs";
|
|
95780
|
+
import { tmpdir } from "os";
|
|
95781
|
+
import { join as join16 } from "path";
|
|
95253
95782
|
function buildWorkflowRunPlan(workflow, options) {
|
|
95254
95783
|
const runOptions = {
|
|
95255
95784
|
url: options.url,
|
|
@@ -95266,10 +95795,10 @@ function buildWorkflowRunPlan(workflow, options) {
|
|
|
95266
95795
|
return {
|
|
95267
95796
|
workflow,
|
|
95268
95797
|
runOptions,
|
|
95269
|
-
|
|
95798
|
+
sandbox: workflow.execution.target === "sandbox" ? buildSandboxPlan(workflow, workflow.execution, runOptions) : null
|
|
95270
95799
|
};
|
|
95271
95800
|
}
|
|
95272
|
-
async function runTestingWorkflow(workflowId, options) {
|
|
95801
|
+
async function runTestingWorkflow(workflowId, options, dependencies = {}) {
|
|
95273
95802
|
const workflow = getTestingWorkflow(workflowId);
|
|
95274
95803
|
if (!workflow)
|
|
95275
95804
|
throw new Error(`Testing workflow not found: ${workflowId}`);
|
|
@@ -95279,13 +95808,25 @@ async function runTestingWorkflow(workflowId, options) {
|
|
|
95279
95808
|
const plan = buildWorkflowRunPlan(workflow, options);
|
|
95280
95809
|
if (options.dryRun)
|
|
95281
95810
|
return { run: null, results: [], plan };
|
|
95282
|
-
if (workflow.execution.target === "
|
|
95283
|
-
const
|
|
95284
|
-
return { run: null, results: [], plan,
|
|
95811
|
+
if (workflow.execution.target === "sandbox") {
|
|
95812
|
+
const sandboxResult = await runViaSandbox(plan, dependencies);
|
|
95813
|
+
return { run: null, results: [], plan, sandboxResult };
|
|
95285
95814
|
}
|
|
95286
|
-
const
|
|
95815
|
+
const runLocal = dependencies.runByFilter ?? runByFilter;
|
|
95816
|
+
const { run, results } = await runLocal(plan.runOptions);
|
|
95287
95817
|
return { run, results, plan };
|
|
95288
95818
|
}
|
|
95819
|
+
function createWorkflowDatabaseBundle(workflow, plan) {
|
|
95820
|
+
if (!plan.sandbox)
|
|
95821
|
+
throw new Error(`Workflow is not configured for sandbox execution: ${workflow.name}`);
|
|
95822
|
+
const localDir = mkdtempSync(join16(tmpdir(), `testers-workflow-${workflow.id.slice(0, 8)}-`));
|
|
95823
|
+
writeFileSync4(join16(localDir, "testers.db"), getDatabase().serialize());
|
|
95824
|
+
return {
|
|
95825
|
+
localDir,
|
|
95826
|
+
remoteDir: plan.sandbox.stateRemoteDir,
|
|
95827
|
+
cleanup: () => rmSync(localDir, { recursive: true, force: true })
|
|
95828
|
+
};
|
|
95829
|
+
}
|
|
95289
95830
|
function validatePersonaIds(workflow) {
|
|
95290
95831
|
for (const personaId of workflow.personaIds) {
|
|
95291
95832
|
if (!getPersona(personaId)) {
|
|
@@ -95293,46 +95834,109 @@ function validatePersonaIds(workflow) {
|
|
|
95293
95834
|
}
|
|
95294
95835
|
}
|
|
95295
95836
|
}
|
|
95296
|
-
function
|
|
95297
|
-
const
|
|
95298
|
-
const
|
|
95299
|
-
|
|
95300
|
-
|
|
95301
|
-
|
|
95837
|
+
function buildSandboxPlan(workflow, execution, runOptions) {
|
|
95838
|
+
const remoteDir = execution.sandboxRemoteDir ?? `/tmp/testers-workflow-${workflow.id.slice(0, 8)}`;
|
|
95839
|
+
const stateRemoteDir = `${remoteDir.replace(/\/+$/, "")}/.testers-state`;
|
|
95840
|
+
return {
|
|
95841
|
+
provider: execution.provider,
|
|
95842
|
+
image: execution.sandboxImage,
|
|
95843
|
+
name: `testers-${workflow.id.slice(0, 8)}`,
|
|
95844
|
+
remoteDir,
|
|
95845
|
+
stateRemoteDir,
|
|
95846
|
+
cleanup: execution.sandboxCleanup ?? "delete",
|
|
95302
95847
|
timeoutMs: execution.timeoutMs,
|
|
95303
|
-
env: execution.env
|
|
95304
|
-
command:
|
|
95305
|
-
|
|
95306
|
-
|
|
95307
|
-
|
|
95308
|
-
|
|
95309
|
-
|
|
95310
|
-
|
|
95311
|
-
|
|
95312
|
-
...runOptions.projectId ? ["--project", runOptions.projectId] : [],
|
|
95313
|
-
...runOptions.model ? ["--model", runOptions.model] : [],
|
|
95314
|
-
"--json"
|
|
95315
|
-
]
|
|
95316
|
-
});
|
|
95317
|
-
return ["connectors", "run", connector, operation, payload];
|
|
95848
|
+
env: execution.env,
|
|
95849
|
+
command: buildSandboxCommand({
|
|
95850
|
+
runOptions,
|
|
95851
|
+
remoteDir,
|
|
95852
|
+
dbPath: `${stateRemoteDir}/testers.db`,
|
|
95853
|
+
setupCommand: execution.setupCommand,
|
|
95854
|
+
packageSpec: execution.packageSpec ?? "@hasna/testers"
|
|
95855
|
+
})
|
|
95856
|
+
};
|
|
95318
95857
|
}
|
|
95319
|
-
|
|
95320
|
-
|
|
95321
|
-
|
|
95322
|
-
|
|
95323
|
-
|
|
95324
|
-
|
|
95325
|
-
|
|
95326
|
-
|
|
95327
|
-
|
|
95328
|
-
|
|
95329
|
-
|
|
95330
|
-
|
|
95331
|
-
|
|
95332
|
-
|
|
95333
|
-
|
|
95858
|
+
function buildSandboxCommand(input) {
|
|
95859
|
+
const args = [
|
|
95860
|
+
"bunx",
|
|
95861
|
+
input.packageSpec,
|
|
95862
|
+
"run",
|
|
95863
|
+
input.runOptions.url,
|
|
95864
|
+
...input.runOptions.scenarioIds?.length ? ["--scenario", input.runOptions.scenarioIds.join(",")] : [],
|
|
95865
|
+
...input.runOptions.tags?.length ? input.runOptions.tags.flatMap((tag) => ["--tag", tag]) : [],
|
|
95866
|
+
...input.runOptions.priority ? ["--priority", input.runOptions.priority] : [],
|
|
95867
|
+
...input.runOptions.projectId ? ["--project", input.runOptions.projectId] : [],
|
|
95868
|
+
...input.runOptions.model ? ["--model", input.runOptions.model] : [],
|
|
95869
|
+
...input.runOptions.headed ? ["--headed"] : [],
|
|
95870
|
+
...input.runOptions.parallel ? ["--parallel", String(input.runOptions.parallel)] : [],
|
|
95871
|
+
...input.runOptions.timeout ? ["--timeout", String(input.runOptions.timeout)] : [],
|
|
95872
|
+
...input.runOptions.personaIds?.length ? ["--persona", input.runOptions.personaIds.join(",")] : [],
|
|
95873
|
+
"--no-auto-generate",
|
|
95874
|
+
"--json"
|
|
95875
|
+
];
|
|
95876
|
+
return [
|
|
95877
|
+
"set -euo pipefail",
|
|
95878
|
+
`mkdir -p ${shellQuote(input.remoteDir)}`,
|
|
95879
|
+
`cd ${shellQuote(input.remoteDir)}`,
|
|
95880
|
+
input.setupCommand,
|
|
95881
|
+
`HASNA_TESTERS_DB_PATH=${shellQuote(input.dbPath)} ${args.map(shellQuote).join(" ")}`
|
|
95882
|
+
].filter(Boolean).join(`
|
|
95883
|
+
`);
|
|
95884
|
+
}
|
|
95885
|
+
async function runViaSandbox(plan, dependencies) {
|
|
95886
|
+
if (!plan.sandbox)
|
|
95887
|
+
throw new Error("Workflow does not have a sandbox plan");
|
|
95888
|
+
const sandboxes = await resolveSandboxesRuntime(dependencies);
|
|
95889
|
+
const createBundle = dependencies.createDatabaseBundle ?? createWorkflowDatabaseBundle;
|
|
95890
|
+
const bundle = createBundle(plan.workflow, plan);
|
|
95891
|
+
try {
|
|
95892
|
+
const raw = await sandboxes.runCommandInSandbox({
|
|
95893
|
+
command: plan.sandbox.command,
|
|
95894
|
+
provider: plan.sandbox.provider,
|
|
95895
|
+
name: plan.sandbox.name,
|
|
95896
|
+
image: plan.sandbox.image,
|
|
95897
|
+
sandboxTimeout: plan.sandbox.timeoutMs,
|
|
95898
|
+
commandTimeoutMs: plan.sandbox.timeoutMs,
|
|
95899
|
+
projectId: plan.workflow.projectId ?? undefined,
|
|
95900
|
+
config: {
|
|
95901
|
+
source: "testers",
|
|
95902
|
+
workflowId: plan.workflow.id,
|
|
95903
|
+
workflowName: plan.workflow.name
|
|
95904
|
+
},
|
|
95905
|
+
sandboxEnvVars: plan.sandbox.env,
|
|
95906
|
+
cleanup: plan.sandbox.cleanup,
|
|
95907
|
+
upload: {
|
|
95908
|
+
localDir: bundle.localDir,
|
|
95909
|
+
remoteDir: bundle.remoteDir
|
|
95910
|
+
}
|
|
95911
|
+
});
|
|
95912
|
+
const exitCode = raw.result.exit_code ?? raw.result.exitCode ?? 0;
|
|
95913
|
+
const stdout = raw.result.stdout ?? "";
|
|
95914
|
+
const stderr = raw.result.stderr ?? "";
|
|
95915
|
+
if (exitCode !== 0) {
|
|
95916
|
+
throw new Error(`Sandbox workflow execution failed (${exitCode}): ${stderr || stdout}`);
|
|
95917
|
+
}
|
|
95918
|
+
return {
|
|
95919
|
+
sandboxId: raw.sandbox.id,
|
|
95920
|
+
sessionId: raw.session.id,
|
|
95921
|
+
exitCode,
|
|
95922
|
+
stdout,
|
|
95923
|
+
stderr,
|
|
95924
|
+
cleanup: raw.cleanup
|
|
95925
|
+
};
|
|
95926
|
+
} finally {
|
|
95927
|
+
bundle.cleanup?.();
|
|
95334
95928
|
}
|
|
95335
|
-
|
|
95929
|
+
}
|
|
95930
|
+
async function resolveSandboxesRuntime(dependencies) {
|
|
95931
|
+
if (dependencies.sandboxes)
|
|
95932
|
+
return dependencies.sandboxes;
|
|
95933
|
+
if (dependencies.createSandboxesSDK)
|
|
95934
|
+
return dependencies.createSandboxesSDK();
|
|
95935
|
+
const mod = await import("@hasna/sandboxes");
|
|
95936
|
+
return mod.createSandboxesSDK();
|
|
95937
|
+
}
|
|
95938
|
+
function shellQuote(value) {
|
|
95939
|
+
return `'${value.replaceAll("'", `'"'"'`)}'`;
|
|
95336
95940
|
}
|
|
95337
95941
|
|
|
95338
95942
|
// src/db/environments.ts
|
|
@@ -95405,111 +96009,19 @@ function getDefaultEnvironment() {
|
|
|
95405
96009
|
|
|
95406
96010
|
// src/cli/index.tsx
|
|
95407
96011
|
init_ci();
|
|
95408
|
-
|
|
95409
|
-
// src/lib/assertions.ts
|
|
95410
|
-
function parseAssertionString(str) {
|
|
95411
|
-
const trimmed = str.trim();
|
|
95412
|
-
if (trimmed === "no-console-errors") {
|
|
95413
|
-
return { type: "no_console_errors", description: "No console errors" };
|
|
95414
|
-
}
|
|
95415
|
-
if (trimmed.startsWith("url:contains:")) {
|
|
95416
|
-
const expected = trimmed.slice("url:contains:".length);
|
|
95417
|
-
return { type: "url_contains", expected, description: `URL contains "${expected}"` };
|
|
95418
|
-
}
|
|
95419
|
-
if (trimmed.startsWith("title:contains:")) {
|
|
95420
|
-
const expected = trimmed.slice("title:contains:".length);
|
|
95421
|
-
return { type: "title_contains", expected, description: `Title contains "${expected}"` };
|
|
95422
|
-
}
|
|
95423
|
-
if (trimmed.startsWith("count:")) {
|
|
95424
|
-
const rest = trimmed.slice("count:".length);
|
|
95425
|
-
const eqIdx = rest.indexOf(" eq:");
|
|
95426
|
-
if (eqIdx === -1) {
|
|
95427
|
-
throw new Error(`Invalid count assertion format: ${str}. Expected "count:<selector> eq:<number>"`);
|
|
95428
|
-
}
|
|
95429
|
-
const selector = rest.slice(0, eqIdx);
|
|
95430
|
-
const expected = parseInt(rest.slice(eqIdx + " eq:".length), 10);
|
|
95431
|
-
return { type: "element_count", selector, expected, description: `${selector} count equals ${expected}` };
|
|
95432
|
-
}
|
|
95433
|
-
if (trimmed.startsWith("text:")) {
|
|
95434
|
-
const rest = trimmed.slice("text:".length);
|
|
95435
|
-
const containsIdx = rest.indexOf(" contains:");
|
|
95436
|
-
const equalsIdx = rest.indexOf(" equals:");
|
|
95437
|
-
if (containsIdx !== -1) {
|
|
95438
|
-
const selector = rest.slice(0, containsIdx);
|
|
95439
|
-
const expected = rest.slice(containsIdx + " contains:".length);
|
|
95440
|
-
return { type: "text_contains", selector, expected, description: `${selector} text contains "${expected}"` };
|
|
95441
|
-
}
|
|
95442
|
-
if (equalsIdx !== -1) {
|
|
95443
|
-
const selector = rest.slice(0, equalsIdx);
|
|
95444
|
-
const expected = rest.slice(equalsIdx + " equals:".length);
|
|
95445
|
-
return { type: "text_equals", selector, expected, description: `${selector} text equals "${expected}"` };
|
|
95446
|
-
}
|
|
95447
|
-
throw new Error(`Invalid text assertion format: ${str}. Expected "text:<selector> contains:<text>" or "text:<selector> equals:<text>"`);
|
|
95448
|
-
}
|
|
95449
|
-
if (trimmed.startsWith("selector:")) {
|
|
95450
|
-
const rest = trimmed.slice("selector:".length);
|
|
95451
|
-
const lastSpace = rest.lastIndexOf(" ");
|
|
95452
|
-
if (lastSpace === -1) {
|
|
95453
|
-
throw new Error(`Invalid selector assertion format: ${str}. Expected "selector:<selector> visible" or "selector:<selector> not-visible"`);
|
|
95454
|
-
}
|
|
95455
|
-
const selector = rest.slice(0, lastSpace);
|
|
95456
|
-
const action = rest.slice(lastSpace + 1);
|
|
95457
|
-
if (action === "visible") {
|
|
95458
|
-
return { type: "visible", selector, description: `${selector} is visible` };
|
|
95459
|
-
}
|
|
95460
|
-
if (action === "not-visible") {
|
|
95461
|
-
return { type: "not_visible", selector, description: `${selector} is not visible` };
|
|
95462
|
-
}
|
|
95463
|
-
throw new Error(`Unknown selector action: "${action}". Expected "visible" or "not-visible"`);
|
|
95464
|
-
}
|
|
95465
|
-
if (trimmed.startsWith("cookie:exists:")) {
|
|
95466
|
-
const name = trimmed.slice("cookie:exists:".length);
|
|
95467
|
-
return { type: "cookie_exists", expected: name, description: `Cookie "${name}" exists` };
|
|
95468
|
-
}
|
|
95469
|
-
if (trimmed.startsWith("cookie:not-exists:")) {
|
|
95470
|
-
const name = trimmed.slice("cookie:not-exists:".length);
|
|
95471
|
-
return { type: "cookie_not_exists", expected: name, description: `Cookie "${name}" does not exist` };
|
|
95472
|
-
}
|
|
95473
|
-
if (trimmed.startsWith("cookie:value:")) {
|
|
95474
|
-
const valueStr = trimmed.slice("cookie:value:".length);
|
|
95475
|
-
return { type: "cookie_value", expected: valueStr, description: `Cookie value is "${valueStr}"` };
|
|
95476
|
-
}
|
|
95477
|
-
if (trimmed.startsWith("local:exists:")) {
|
|
95478
|
-
const key = trimmed.slice("local:exists:".length);
|
|
95479
|
-
return { type: "local_storage_exists", expected: key, description: `LocalStorage key "${key}" exists` };
|
|
95480
|
-
}
|
|
95481
|
-
if (trimmed.startsWith("local:not-exists:")) {
|
|
95482
|
-
const key = trimmed.slice("local:not-exists:".length);
|
|
95483
|
-
return { type: "local_storage_not_exists", expected: key, description: `LocalStorage key "${key}" does not exist` };
|
|
95484
|
-
}
|
|
95485
|
-
if (trimmed.startsWith("local:value:")) {
|
|
95486
|
-
const valueStr = trimmed.slice("local:value:".length);
|
|
95487
|
-
return { type: "local_storage_value", expected: valueStr, description: `LocalStorage value is "${valueStr}"` };
|
|
95488
|
-
}
|
|
95489
|
-
if (trimmed.startsWith("session:value:")) {
|
|
95490
|
-
const valueStr = trimmed.slice("session:value:".length);
|
|
95491
|
-
return { type: "session_storage_value", expected: valueStr, description: `SessionStorage value is "${valueStr}"` };
|
|
95492
|
-
}
|
|
95493
|
-
if (trimmed.startsWith("session:not-exists:")) {
|
|
95494
|
-
const key = trimmed.slice("session:not-exists:".length);
|
|
95495
|
-
return { type: "session_storage_not_exists", expected: key, description: `SessionStorage key "${key}" does not exist` };
|
|
95496
|
-
}
|
|
95497
|
-
throw new Error(`Cannot parse assertion: "${str}". See --help for assertion formats.`);
|
|
95498
|
-
}
|
|
95499
|
-
|
|
95500
|
-
// src/cli/index.tsx
|
|
96012
|
+
init_assertions();
|
|
95501
96013
|
init_paths();
|
|
95502
96014
|
init_sessions();
|
|
95503
96015
|
import { existsSync as existsSync17, mkdirSync as mkdirSync14 } from "fs";
|
|
95504
96016
|
|
|
95505
96017
|
// src/lib/repo-discovery.ts
|
|
95506
96018
|
init_paths();
|
|
95507
|
-
import { existsSync as existsSync14, readFileSync as readFileSync6, readdirSync as readdirSync3, statSync, writeFileSync as
|
|
96019
|
+
import { existsSync as existsSync14, readFileSync as readFileSync6, readdirSync as readdirSync3, statSync, writeFileSync as writeFileSync5, mkdirSync as mkdirSync11, unlinkSync } from "fs";
|
|
95508
96020
|
import { createHash } from "crypto";
|
|
95509
|
-
import { join as
|
|
96021
|
+
import { join as join17, resolve, relative as relative2 } from "path";
|
|
95510
96022
|
function getCacheDir() {
|
|
95511
96023
|
const testersDir = getTestersDir();
|
|
95512
|
-
const cacheDir =
|
|
96024
|
+
const cacheDir = join17(testersDir, "repo-index");
|
|
95513
96025
|
if (!existsSync14(cacheDir)) {
|
|
95514
96026
|
mkdirSync11(cacheDir, { recursive: true });
|
|
95515
96027
|
}
|
|
@@ -95519,11 +96031,11 @@ function pathHash(repoPath) {
|
|
|
95519
96031
|
return createHash("sha256").update(repoPath).digest("hex").slice(0, 16);
|
|
95520
96032
|
}
|
|
95521
96033
|
function getCachePath(repoPath) {
|
|
95522
|
-
return
|
|
96034
|
+
return join17(getCacheDir(), `${pathHash(repoPath)}.json`);
|
|
95523
96035
|
}
|
|
95524
96036
|
function isCacheStale(cached, repoPath) {
|
|
95525
96037
|
for (const spec of cached.specs) {
|
|
95526
|
-
const fullPath =
|
|
96038
|
+
const fullPath = join17(repoPath, spec.file);
|
|
95527
96039
|
if (!existsSync14(fullPath))
|
|
95528
96040
|
return true;
|
|
95529
96041
|
try {
|
|
@@ -95535,11 +96047,11 @@ function isCacheStale(cached, repoPath) {
|
|
|
95535
96047
|
}
|
|
95536
96048
|
}
|
|
95537
96049
|
if (cached.configPath) {
|
|
95538
|
-
const configFullPath =
|
|
96050
|
+
const configFullPath = join17(repoPath, cached.configPath);
|
|
95539
96051
|
if (!existsSync14(configFullPath))
|
|
95540
96052
|
return true;
|
|
95541
96053
|
try {
|
|
95542
|
-
|
|
96054
|
+
statSync(configFullPath);
|
|
95543
96055
|
const age = Date.now() - new Date(cached.snapshotAt).getTime();
|
|
95544
96056
|
if (age > 3600000)
|
|
95545
96057
|
return true;
|
|
@@ -95562,14 +96074,14 @@ function loadCache(repoPath) {
|
|
|
95562
96074
|
}
|
|
95563
96075
|
function saveCache(snapshot) {
|
|
95564
96076
|
const cachePath = getCachePath(snapshot.repoPath);
|
|
95565
|
-
|
|
96077
|
+
writeFileSync5(cachePath, JSON.stringify(snapshot, null, 2), "utf-8");
|
|
95566
96078
|
}
|
|
95567
96079
|
function detectPackageManager(repoPath) {
|
|
95568
96080
|
const result = {
|
|
95569
|
-
npm: existsSync14(
|
|
95570
|
-
yarn: existsSync14(
|
|
95571
|
-
pnpm: existsSync14(
|
|
95572
|
-
bun: existsSync14(
|
|
96081
|
+
npm: existsSync14(join17(repoPath, "package-lock.json")),
|
|
96082
|
+
yarn: existsSync14(join17(repoPath, "yarn.lock")),
|
|
96083
|
+
pnpm: existsSync14(join17(repoPath, "pnpm-lock.yaml")),
|
|
96084
|
+
bun: existsSync14(join17(repoPath, "bun.lockb")) || existsSync14(join17(repoPath, "bun.lock")),
|
|
95573
96085
|
preferred: "npm"
|
|
95574
96086
|
};
|
|
95575
96087
|
if (result.bun)
|
|
@@ -95583,7 +96095,7 @@ function detectPackageManager(repoPath) {
|
|
|
95583
96095
|
return result;
|
|
95584
96096
|
}
|
|
95585
96097
|
function detectDevScripts(repoPath) {
|
|
95586
|
-
const pkgPath =
|
|
96098
|
+
const pkgPath = join17(repoPath, "package.json");
|
|
95587
96099
|
if (!existsSync14(pkgPath)) {
|
|
95588
96100
|
return { dev: null, test: null, seed: null, build: null };
|
|
95589
96101
|
}
|
|
@@ -95610,7 +96122,7 @@ function findPlaywrightConfig(repoPath) {
|
|
|
95610
96122
|
"playwright-ct.config.js"
|
|
95611
96123
|
];
|
|
95612
96124
|
for (const name of candidates) {
|
|
95613
|
-
if (existsSync14(
|
|
96125
|
+
if (existsSync14(join17(repoPath, name)))
|
|
95614
96126
|
return name;
|
|
95615
96127
|
}
|
|
95616
96128
|
return null;
|
|
@@ -95619,7 +96131,7 @@ function extractTestGlobPatterns(configPath, repoPath) {
|
|
|
95619
96131
|
if (!configPath) {
|
|
95620
96132
|
return ["**/*.spec.ts", "**/*.spec.js", "**/*.test.ts", "**/*.test.js", "**/e2e/**/*.ts", "**/e2e/**/*.js", "**/tests/**/*.ts", "**/tests/**/*.js"];
|
|
95621
96133
|
}
|
|
95622
|
-
const fullPath =
|
|
96134
|
+
const fullPath = join17(repoPath, configPath);
|
|
95623
96135
|
let content;
|
|
95624
96136
|
try {
|
|
95625
96137
|
content = readFileSync6(fullPath, "utf-8");
|
|
@@ -95630,8 +96142,9 @@ function extractTestGlobPatterns(configPath, repoPath) {
|
|
|
95630
96142
|
const testDirMatch = content.match(/testDir\s*[:=]\s*['"`]([^'"`]+)['"`]/);
|
|
95631
96143
|
const testDir = testDirMatch?.[1];
|
|
95632
96144
|
const testMatchArray = content.match(/testMatch\s*[:=]\s*\[([^\]]+)\]/);
|
|
95633
|
-
|
|
95634
|
-
|
|
96145
|
+
const testMatchBody = testMatchArray?.[1];
|
|
96146
|
+
if (testMatchBody) {
|
|
96147
|
+
const items = testMatchBody.match(/['"`]([^'"`]+)['"`]/g);
|
|
95635
96148
|
if (items) {
|
|
95636
96149
|
for (const item of items) {
|
|
95637
96150
|
patterns.push(item.replace(/['"`]/g, ""));
|
|
@@ -95639,8 +96152,9 @@ function extractTestGlobPatterns(configPath, repoPath) {
|
|
|
95639
96152
|
}
|
|
95640
96153
|
}
|
|
95641
96154
|
const testMatchSingle = content.match(/testMatch\s*[:=]\s*['"`]([^'"`]+)['"`]/);
|
|
95642
|
-
|
|
95643
|
-
|
|
96155
|
+
const singleTestMatch = testMatchSingle?.[1];
|
|
96156
|
+
if (singleTestMatch) {
|
|
96157
|
+
patterns.push(singleTestMatch);
|
|
95644
96158
|
}
|
|
95645
96159
|
if (testDir && patterns.length === 0) {
|
|
95646
96160
|
patterns.push(`${testDir}/**/*.spec.ts`, `${testDir}/**/*.test.ts`, `${testDir}/**/*.spec.js`, `${testDir}/**/*.test.js`);
|
|
@@ -95666,7 +96180,7 @@ function findSpecFiles(repoPath, globPatterns) {
|
|
|
95666
96180
|
for (const pattern of globPatterns) {
|
|
95667
96181
|
const dirsToSearch = ["", ".", "tests", "e2e", "test", "__tests__", "specs", "src"];
|
|
95668
96182
|
for (const dir of dirsToSearch) {
|
|
95669
|
-
const searchDir = dir ?
|
|
96183
|
+
const searchDir = dir ? join17(repoPath, dir) : repoPath;
|
|
95670
96184
|
if (!existsSync14(searchDir))
|
|
95671
96185
|
continue;
|
|
95672
96186
|
try {
|
|
@@ -95700,7 +96214,7 @@ function walkDir(dir) {
|
|
|
95700
96214
|
try {
|
|
95701
96215
|
const entries = readdirSync3(dir, { withFileTypes: true });
|
|
95702
96216
|
for (const entry of entries) {
|
|
95703
|
-
const fullPath =
|
|
96217
|
+
const fullPath = join17(dir, entry.name);
|
|
95704
96218
|
if (entry.isDirectory()) {
|
|
95705
96219
|
if (entry.name === "node_modules" || entry.name === ".git")
|
|
95706
96220
|
continue;
|
|
@@ -95718,7 +96232,7 @@ function matchesGlob(filePath, pattern) {
|
|
|
95718
96232
|
return new RegExp(regex).test(filePath);
|
|
95719
96233
|
}
|
|
95720
96234
|
function detectSuggestedUrl(repoPath) {
|
|
95721
|
-
const pkgPath =
|
|
96235
|
+
const pkgPath = join17(repoPath, "package.json");
|
|
95722
96236
|
if (!existsSync14(pkgPath))
|
|
95723
96237
|
return null;
|
|
95724
96238
|
try {
|
|
@@ -95738,10 +96252,10 @@ function detectSuggestedUrl(repoPath) {
|
|
|
95738
96252
|
return null;
|
|
95739
96253
|
}
|
|
95740
96254
|
function checkPlaywrightBrowserInstalled(repoPath) {
|
|
95741
|
-
const cacheDir =
|
|
96255
|
+
const cacheDir = join17(repoPath, "node_modules", ".cache", "ms-playwright");
|
|
95742
96256
|
if (existsSync14(cacheDir))
|
|
95743
96257
|
return true;
|
|
95744
|
-
const globalCache =
|
|
96258
|
+
const globalCache = join17(repoPath, ".cache", "ms-playwright");
|
|
95745
96259
|
if (existsSync14(globalCache))
|
|
95746
96260
|
return true;
|
|
95747
96261
|
return false;
|
|
@@ -95758,7 +96272,7 @@ function getInstallCommand(pm) {
|
|
|
95758
96272
|
return "bun install";
|
|
95759
96273
|
}
|
|
95760
96274
|
}
|
|
95761
|
-
function getPlaywrightInstallCommand(
|
|
96275
|
+
function getPlaywrightInstallCommand(_pm) {
|
|
95762
96276
|
return "npx playwright install";
|
|
95763
96277
|
}
|
|
95764
96278
|
function discoverRepo(opts) {
|
|
@@ -95773,7 +96287,7 @@ function discoverRepo(opts) {
|
|
|
95773
96287
|
let configRaw = null;
|
|
95774
96288
|
if (configPath) {
|
|
95775
96289
|
try {
|
|
95776
|
-
configRaw = readFileSync6(
|
|
96290
|
+
configRaw = readFileSync6(join17(repoPath, configPath), "utf-8");
|
|
95777
96291
|
} catch {
|
|
95778
96292
|
configRaw = null;
|
|
95779
96293
|
}
|
|
@@ -95782,7 +96296,7 @@ function discoverRepo(opts) {
|
|
|
95782
96296
|
const specs = findSpecFiles(repoPath, globPatterns);
|
|
95783
96297
|
const packageManager = detectPackageManager(repoPath);
|
|
95784
96298
|
const devScripts = detectDevScripts(repoPath);
|
|
95785
|
-
const playwrightInstalled = existsSync14(
|
|
96299
|
+
const playwrightInstalled = existsSync14(join17(repoPath, "node_modules", "playwright")) || existsSync14(join17(repoPath, "node_modules", "@playwright", "test"));
|
|
95786
96300
|
const browsersInstalled = checkPlaywrightBrowserInstalled(repoPath);
|
|
95787
96301
|
const configExists = configPath !== null;
|
|
95788
96302
|
const specsFound = specs.length > 0;
|
|
@@ -95851,7 +96365,7 @@ function clearDiscoveryCache(repoPath) {
|
|
|
95851
96365
|
} else {
|
|
95852
96366
|
for (const file of readdirSync3(cacheDir)) {
|
|
95853
96367
|
if (file.endsWith(".json")) {
|
|
95854
|
-
unlinkSync(
|
|
96368
|
+
unlinkSync(join17(cacheDir, file));
|
|
95855
96369
|
}
|
|
95856
96370
|
}
|
|
95857
96371
|
}
|
|
@@ -95875,10 +96389,10 @@ init_runs();
|
|
|
95875
96389
|
init_database();
|
|
95876
96390
|
init_paths();
|
|
95877
96391
|
import { execSync as execSync2 } from "child_process";
|
|
95878
|
-
import { existsSync as existsSync15, mkdirSync as mkdirSync12, writeFileSync as
|
|
95879
|
-
import { join as
|
|
96392
|
+
import { existsSync as existsSync15, mkdirSync as mkdirSync12, writeFileSync as writeFileSync6 } from "fs";
|
|
96393
|
+
import { join as join18 } from "path";
|
|
95880
96394
|
function resolvePlaywrightCmd(repoPath) {
|
|
95881
|
-
const localPw =
|
|
96395
|
+
const localPw = join18(repoPath, "node_modules", ".bin", "playwright");
|
|
95882
96396
|
if (existsSync15(localPw)) {
|
|
95883
96397
|
return [localPw, "test"];
|
|
95884
96398
|
}
|
|
@@ -95897,7 +96411,7 @@ function buildPlaywrightArgs(specFiles, extraArgs = []) {
|
|
|
95897
96411
|
}
|
|
95898
96412
|
function runPlaywright(repoPath, workingDir, specFiles, extraArgs, timeoutMs) {
|
|
95899
96413
|
const cmd = resolvePlaywrightCmd(repoPath);
|
|
95900
|
-
const args = buildPlaywrightArgs(specFiles, extraArgs
|
|
96414
|
+
const args = buildPlaywrightArgs(specFiles, extraArgs);
|
|
95901
96415
|
const startTime = Date.now();
|
|
95902
96416
|
try {
|
|
95903
96417
|
const result = execSync2(`${cmd.join(" ")} ${args.join(" ")}`, {
|
|
@@ -95925,7 +96439,7 @@ function runPlaywright(repoPath, workingDir, specFiles, extraArgs, timeoutMs) {
|
|
|
95925
96439
|
};
|
|
95926
96440
|
}
|
|
95927
96441
|
}
|
|
95928
|
-
function parsePlaywrightJsonOutput(stdout,
|
|
96442
|
+
function parsePlaywrightJsonOutput(stdout, _stderr) {
|
|
95929
96443
|
const testResults = [];
|
|
95930
96444
|
try {
|
|
95931
96445
|
const obj = JSON.parse(stdout);
|
|
@@ -96030,19 +96544,21 @@ async function runRepoTests(opts) {
|
|
|
96030
96544
|
const workingDir = opts.snapshot.workingDir;
|
|
96031
96545
|
const repoPath = snapshot.repoPath;
|
|
96032
96546
|
const url = opts.url ?? snapshot.suggestedUrl ?? "http://localhost:3000";
|
|
96033
|
-
const
|
|
96547
|
+
const initialRun = createRun({
|
|
96034
96548
|
projectId: opts.projectId,
|
|
96035
96549
|
url,
|
|
96036
96550
|
model: opts.model ?? "repo-native",
|
|
96037
96551
|
headed: false,
|
|
96038
|
-
parallel: 1
|
|
96039
|
-
|
|
96552
|
+
parallel: 1
|
|
96553
|
+
});
|
|
96554
|
+
const run = updateRun(initialRun.id, {
|
|
96555
|
+
metadata: JSON.stringify({
|
|
96040
96556
|
runType: "repo-native",
|
|
96041
96557
|
repoPath,
|
|
96042
96558
|
configPath: snapshot.configPath,
|
|
96043
96559
|
cacheKey: snapshot.cacheKey,
|
|
96044
96560
|
label: opts.label
|
|
96045
|
-
}
|
|
96561
|
+
})
|
|
96046
96562
|
});
|
|
96047
96563
|
const specResults = [];
|
|
96048
96564
|
const startTime = Date.now();
|
|
@@ -96072,10 +96588,10 @@ async function runRepoTests(opts) {
|
|
|
96072
96588
|
}
|
|
96073
96589
|
const resultRecord = { id: resultId };
|
|
96074
96590
|
if (result.stdout || result.stderr) {
|
|
96075
|
-
const reportersDir =
|
|
96591
|
+
const reportersDir = join18(getTestersDir(), "repo-run-output");
|
|
96076
96592
|
mkdirSync12(reportersDir, { recursive: true });
|
|
96077
|
-
const outputFile =
|
|
96078
|
-
|
|
96593
|
+
const outputFile = join18(reportersDir, `${resultRecord.id}.log`);
|
|
96594
|
+
writeFileSync6(outputFile, `=== stdout ===
|
|
96079
96595
|
${result.stdout}
|
|
96080
96596
|
|
|
96081
96597
|
=== stderr ===
|
|
@@ -96182,6 +96698,10 @@ function processSyncEnv() {
|
|
|
96182
96698
|
// src/cli/index.tsx
|
|
96183
96699
|
import { jsxDEV } from "react/jsx-dev-runtime";
|
|
96184
96700
|
var PRIORITIES = ["low", "medium", "high", "critical"];
|
|
96701
|
+
function splitCsvOption(value) {
|
|
96702
|
+
const items = value?.split(",").map((item) => item.trim()).filter(Boolean) ?? [];
|
|
96703
|
+
return items.length > 0 ? items : undefined;
|
|
96704
|
+
}
|
|
96185
96705
|
function AddForm({ onComplete }) {
|
|
96186
96706
|
const { exit } = useApp();
|
|
96187
96707
|
const [state, setState] = useState({
|
|
@@ -96455,25 +96975,30 @@ program2.command("prod-debug <target>").description("Create a safe production de
|
|
|
96455
96975
|
}, config2.prodDebug);
|
|
96456
96976
|
const output = opts.json ? JSON.stringify(plan, null, 2) : formatProdDebugPlan(plan);
|
|
96457
96977
|
if (opts.output) {
|
|
96458
|
-
|
|
96978
|
+
writeFileSync8(resolve4(opts.output), output + `
|
|
96459
96979
|
`);
|
|
96460
96980
|
} else {
|
|
96461
96981
|
log(output);
|
|
96462
96982
|
}
|
|
96463
96983
|
});
|
|
96464
96984
|
var CONFIG_DIR5 = getTestersDir();
|
|
96465
|
-
var CONFIG_PATH4 =
|
|
96985
|
+
var CONFIG_PATH4 = join20(CONFIG_DIR5, "config.json");
|
|
96466
96986
|
function getActiveProject() {
|
|
96467
96987
|
try {
|
|
96468
96988
|
if (existsSync17(CONFIG_PATH4)) {
|
|
96469
|
-
const raw = JSON.parse(
|
|
96989
|
+
const raw = JSON.parse(readFileSync10(CONFIG_PATH4, "utf-8"));
|
|
96470
96990
|
return raw.activeProject ?? undefined;
|
|
96471
96991
|
}
|
|
96472
96992
|
} catch {}
|
|
96473
96993
|
return;
|
|
96474
96994
|
}
|
|
96475
96995
|
function resolveProject2(optProject) {
|
|
96476
|
-
|
|
96996
|
+
if (optProject)
|
|
96997
|
+
return optProject;
|
|
96998
|
+
const activeProject = getActiveProject();
|
|
96999
|
+
if (!activeProject)
|
|
97000
|
+
return;
|
|
97001
|
+
return getProject(activeProject) ? activeProject : undefined;
|
|
96477
97002
|
}
|
|
96478
97003
|
program2.command("add [name]").alias("create").description("Create a new test scenario (interactive if no name/flags given)").option("-d, --description <text>", "Scenario description", "").option("-s, --steps <step>", "Test step (repeatable)", (val, acc) => {
|
|
96479
97004
|
acc.push(val);
|
|
@@ -96658,7 +97183,7 @@ program2.command("delete <id>").description("Delete a scenario").option("-y, --y
|
|
|
96658
97183
|
}
|
|
96659
97184
|
if (!opts.yes) {
|
|
96660
97185
|
process.stdout.write(chalk6.yellow(`Delete scenario ${scenario.shortId} "${scenario.name}"? [y/N] `));
|
|
96661
|
-
const answer = await new Promise((
|
|
97186
|
+
const answer = await new Promise((resolve5) => {
|
|
96662
97187
|
let buf = "";
|
|
96663
97188
|
process.stdin.setRawMode?.(true);
|
|
96664
97189
|
process.stdin.resume();
|
|
@@ -96668,7 +97193,7 @@ program2.command("delete <id>").description("Delete a scenario").option("-y, --y
|
|
|
96668
97193
|
process.stdin.pause();
|
|
96669
97194
|
process.stdout.write(`
|
|
96670
97195
|
`);
|
|
96671
|
-
|
|
97196
|
+
resolve5(buf);
|
|
96672
97197
|
});
|
|
96673
97198
|
});
|
|
96674
97199
|
if (answer !== "y" && answer !== "yes") {
|
|
@@ -96697,7 +97222,7 @@ program2.command("remove <id>").alias("uninstall").description("Remove a scenari
|
|
|
96697
97222
|
}
|
|
96698
97223
|
if (!opts.yes) {
|
|
96699
97224
|
process.stdout.write(chalk6.yellow(`Remove scenario ${scenario.shortId} "${scenario.name}"? [y/N] `));
|
|
96700
|
-
const answer = await new Promise((
|
|
97225
|
+
const answer = await new Promise((resolve5) => {
|
|
96701
97226
|
let buf = "";
|
|
96702
97227
|
process.stdin.setRawMode?.(true);
|
|
96703
97228
|
process.stdin.resume();
|
|
@@ -96707,7 +97232,7 @@ program2.command("remove <id>").alias("uninstall").description("Remove a scenari
|
|
|
96707
97232
|
process.stdin.pause();
|
|
96708
97233
|
process.stdout.write(`
|
|
96709
97234
|
`);
|
|
96710
|
-
|
|
97235
|
+
resolve5(buf);
|
|
96711
97236
|
});
|
|
96712
97237
|
});
|
|
96713
97238
|
if (answer !== "y" && answer !== "yes") {
|
|
@@ -96753,6 +97278,7 @@ program2.command("run [url] [description]").alias("test").description("Run test
|
|
|
96753
97278
|
logError(chalk6.red("No URL provided. Pass a URL argument, use --env <name>, or set a default environment with 'testers env use <name>'."));
|
|
96754
97279
|
process.exit(2);
|
|
96755
97280
|
}
|
|
97281
|
+
const scenarioIds = splitCsvOption(opts.scenario);
|
|
96756
97282
|
if (!opts.dryRun) {
|
|
96757
97283
|
const hasAnthropic = Boolean(process.env["ANTHROPIC_API_KEY"]);
|
|
96758
97284
|
const hasOpenAI = Boolean(process.env["OPENAI_API_KEY"]);
|
|
@@ -96823,7 +97349,7 @@ program2.command("run [url] [description]").alias("test").description("Run test
|
|
|
96823
97349
|
tags: opts.tag.length > 0 ? opts.tag : undefined,
|
|
96824
97350
|
projectId
|
|
96825
97351
|
}).filter((s2) => {
|
|
96826
|
-
if (
|
|
97352
|
+
if (scenarioIds && !scenarioIds.includes(s2.id) && !scenarioIds.includes(s2.shortId))
|
|
96827
97353
|
return false;
|
|
96828
97354
|
if (opts.priority && s2.priority !== opts.priority)
|
|
96829
97355
|
return false;
|
|
@@ -96872,7 +97398,7 @@ program2.command("run [url] [description]").alias("test").description("Run test
|
|
|
96872
97398
|
const { runId, scenarioCount } = startRunAsync({
|
|
96873
97399
|
url: url2,
|
|
96874
97400
|
tags: opts.tag.length > 0 ? opts.tag : undefined,
|
|
96875
|
-
scenarioIds
|
|
97401
|
+
scenarioIds,
|
|
96876
97402
|
priority: opts.priority,
|
|
96877
97403
|
model: opts.model,
|
|
96878
97404
|
headed: opts.headed,
|
|
@@ -96906,7 +97432,7 @@ program2.command("run [url] [description]").alias("test").description("Run test
|
|
|
96906
97432
|
`);
|
|
96907
97433
|
}
|
|
96908
97434
|
};
|
|
96909
|
-
await new Promise((
|
|
97435
|
+
await new Promise((resolve5) => {
|
|
96910
97436
|
const poll = setInterval(() => {
|
|
96911
97437
|
const run3 = getRun(runId);
|
|
96912
97438
|
if (!run3)
|
|
@@ -96914,7 +97440,7 @@ program2.command("run [url] [description]").alias("test").description("Run test
|
|
|
96914
97440
|
renderTable();
|
|
96915
97441
|
if (DONE_STATUSES.has(run3.status)) {
|
|
96916
97442
|
clearInterval(poll);
|
|
96917
|
-
|
|
97443
|
+
resolve5();
|
|
96918
97444
|
}
|
|
96919
97445
|
}, POLL_INTERVAL);
|
|
96920
97446
|
});
|
|
@@ -97007,7 +97533,7 @@ program2.command("run [url] [description]").alias("test").description("Run test
|
|
|
97007
97533
|
if (opts.json || opts.output) {
|
|
97008
97534
|
const jsonOutput = formatJSON(run3, results2);
|
|
97009
97535
|
if (opts.output) {
|
|
97010
|
-
|
|
97536
|
+
writeFileSync8(opts.output, jsonOutput, "utf-8");
|
|
97011
97537
|
log(chalk6.green(`Results written to ${opts.output}`));
|
|
97012
97538
|
}
|
|
97013
97539
|
if (opts.json) {
|
|
@@ -97030,7 +97556,7 @@ program2.command("run [url] [description]").alias("test").description("Run test
|
|
|
97030
97556
|
}
|
|
97031
97557
|
process.exit(getExitCode(run3));
|
|
97032
97558
|
}
|
|
97033
|
-
const noFilters = !
|
|
97559
|
+
const noFilters = !scenarioIds && opts.tag.length === 0 && !opts.priority;
|
|
97034
97560
|
if (noFilters && !opts.json && !opts.output) {
|
|
97035
97561
|
const allScenarios = listScenarios({ projectId });
|
|
97036
97562
|
log(chalk6.bold(` Running all ${allScenarios.length} scenarios...`));
|
|
@@ -97094,7 +97620,7 @@ program2.command("run [url] [description]").alias("test").description("Run test
|
|
|
97094
97620
|
const { run: run2, results } = await runByFilter({
|
|
97095
97621
|
url: url2,
|
|
97096
97622
|
tags: opts.tag.length > 0 ? opts.tag : undefined,
|
|
97097
|
-
scenarioIds: diffScenarioIds ??
|
|
97623
|
+
scenarioIds: diffScenarioIds ?? scenarioIds,
|
|
97098
97624
|
priority: opts.priority,
|
|
97099
97625
|
model: opts.model,
|
|
97100
97626
|
headed: opts.headed,
|
|
@@ -97116,7 +97642,7 @@ program2.command("run [url] [description]").alias("test").description("Run test
|
|
|
97116
97642
|
if (opts.json || opts.output) {
|
|
97117
97643
|
const jsonOutput = formatJSON(run2, results);
|
|
97118
97644
|
if (opts.output) {
|
|
97119
|
-
|
|
97645
|
+
writeFileSync8(opts.output, jsonOutput, "utf-8");
|
|
97120
97646
|
log(chalk6.green(`Results written to ${opts.output}`));
|
|
97121
97647
|
}
|
|
97122
97648
|
if (opts.json) {
|
|
@@ -97306,7 +97832,7 @@ program2.command("screenshots <id>").description("List screenshots for a run or
|
|
|
97306
97832
|
});
|
|
97307
97833
|
program2.command("import <dir>").description("Import markdown test files as scenarios").action((dir) => {
|
|
97308
97834
|
try {
|
|
97309
|
-
const absDir =
|
|
97835
|
+
const absDir = resolve4(dir);
|
|
97310
97836
|
const files = readdirSync6(absDir).filter((f2) => f2.endsWith(".md"));
|
|
97311
97837
|
if (files.length === 0) {
|
|
97312
97838
|
log(chalk6.dim("No .md files found in directory."));
|
|
@@ -97314,7 +97840,7 @@ program2.command("import <dir>").description("Import markdown test files as scen
|
|
|
97314
97840
|
}
|
|
97315
97841
|
let imported = 0;
|
|
97316
97842
|
for (const file2 of files) {
|
|
97317
|
-
const content =
|
|
97843
|
+
const content = readFileSync10(join20(absDir, file2), "utf-8");
|
|
97318
97844
|
const lines = content.split(`
|
|
97319
97845
|
`);
|
|
97320
97846
|
let name21 = file2.replace(/\.md$/, "");
|
|
@@ -97369,8 +97895,8 @@ program2.command("export [format]").description("Export scenarios as JSON (defau
|
|
|
97369
97895
|
if (fmt === "json") {
|
|
97370
97896
|
const outputPath = opts.output ?? "testers-export.json";
|
|
97371
97897
|
const data = JSON.stringify(scenarios, null, 2);
|
|
97372
|
-
|
|
97373
|
-
log(chalk6.green(`Exported ${scenarios.length} scenario(s) to ${
|
|
97898
|
+
writeFileSync8(outputPath, data, "utf-8");
|
|
97899
|
+
log(chalk6.green(`Exported ${scenarios.length} scenario(s) to ${resolve4(outputPath)}`));
|
|
97374
97900
|
return;
|
|
97375
97901
|
}
|
|
97376
97902
|
const outputDir = opts.output ?? ".";
|
|
@@ -97402,13 +97928,13 @@ program2.command("export [format]").description("Export scenarios as JSON (defau
|
|
|
97402
97928
|
lines.push("");
|
|
97403
97929
|
}
|
|
97404
97930
|
const safeFilename = s2.name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 80);
|
|
97405
|
-
const filePath =
|
|
97406
|
-
|
|
97931
|
+
const filePath = join20(outputDir, `${s2.shortId}-${safeFilename}.md`);
|
|
97932
|
+
writeFileSync8(filePath, lines.join(`
|
|
97407
97933
|
`), "utf-8");
|
|
97408
97934
|
log(chalk6.dim(` ${s2.shortId}: ${s2.name} \u2192 ${filePath}`));
|
|
97409
97935
|
}
|
|
97410
97936
|
log(chalk6.green(`
|
|
97411
|
-
Exported ${scenarios.length} scenario(s) as markdown to ${
|
|
97937
|
+
Exported ${scenarios.length} scenario(s) as markdown to ${resolve4(outputDir)}`));
|
|
97412
97938
|
} catch (error40) {
|
|
97413
97939
|
logError(chalk6.red(`Error: ${error40 instanceof Error ? error40.message : String(error40)}`));
|
|
97414
97940
|
process.exit(1);
|
|
@@ -97427,7 +97953,7 @@ program2.command("status").description("Show database and auth status").action((
|
|
|
97427
97953
|
try {
|
|
97428
97954
|
const config2 = loadConfig();
|
|
97429
97955
|
const hasApiKey = !!config2.anthropicApiKey || !!process.env["ANTHROPIC_API_KEY"];
|
|
97430
|
-
const dbPath =
|
|
97956
|
+
const dbPath = join20(getTestersDir(), "testers.db");
|
|
97431
97957
|
log("");
|
|
97432
97958
|
log(chalk6.bold(" Open Testers Status"));
|
|
97433
97959
|
log("");
|
|
@@ -97581,11 +98107,11 @@ projectCmd.command("use <name>").description("Set active project (find or create
|
|
|
97581
98107
|
let config2 = {};
|
|
97582
98108
|
if (existsSync17(CONFIG_PATH4)) {
|
|
97583
98109
|
try {
|
|
97584
|
-
config2 = JSON.parse(
|
|
98110
|
+
config2 = JSON.parse(readFileSync10(CONFIG_PATH4, "utf-8"));
|
|
97585
98111
|
} catch {}
|
|
97586
98112
|
}
|
|
97587
98113
|
config2.activeProject = project.id;
|
|
97588
|
-
|
|
98114
|
+
writeFileSync8(CONFIG_PATH4, JSON.stringify(config2, null, 2), "utf-8");
|
|
97589
98115
|
if (opts.json) {
|
|
97590
98116
|
log(JSON.stringify({ activeProject: project.id, project }, null, 2));
|
|
97591
98117
|
return;
|
|
@@ -97599,7 +98125,7 @@ projectCmd.command("use <name>").description("Set active project (find or create
|
|
|
97599
98125
|
var repoCmd = program2.command("repo").description("Discover and run repo-native Playwright tests");
|
|
97600
98126
|
repoCmd.command("discover [path]").alias("scan").description("Discover Playwright tests in a repo").option("--refresh", "Force a fresh scan, ignoring cache", false).option("--json", "Output as JSON", false).option("--base-url <url>", "Override the suggested base URL").action((path, opts) => {
|
|
97601
98127
|
try {
|
|
97602
|
-
const repoPath =
|
|
98128
|
+
const repoPath = resolve4(path ?? process.cwd());
|
|
97603
98129
|
const snapshot = discoverRepo({
|
|
97604
98130
|
repoPath,
|
|
97605
98131
|
refresh: opts.refresh,
|
|
@@ -97665,7 +98191,7 @@ repoCmd.command("discover [path]").alias("scan").description("Discover Playwrigh
|
|
|
97665
98191
|
});
|
|
97666
98192
|
repoCmd.command("prepare [path]").alias("prep").description("Install dependencies and browsers for repo tests").option("--all", "Run all prep steps (install, browsers, build, seed)", false).option("--install", "Install dependencies", false).option("--browsers", "Install Playwright browsers", false).option("--build", "Build the app", false).option("--seed", "Seed the database", false).option("--refresh", "Force fresh discovery scan", false).option("--json", "Output as JSON", false).action((path, opts) => {
|
|
97667
98193
|
try {
|
|
97668
|
-
const repoPath =
|
|
98194
|
+
const repoPath = resolve4(path ?? process.cwd());
|
|
97669
98195
|
const snapshot = discoverRepo({ repoPath, refresh: opts.refresh });
|
|
97670
98196
|
const steps = [];
|
|
97671
98197
|
if (opts.all) {
|
|
@@ -97743,7 +98269,7 @@ repoCmd.command("run [path]").description("Run discovered Playwright tests nativ
|
|
|
97743
98269
|
return acc;
|
|
97744
98270
|
}, []).option("--timeout <ms>", "Timeout per spec file", "300000").option("--url <url>", "Dev server URL").option("--project <id>", "Project ID for result storage").option("--label <text>", "Run label").option("--json", "Output as JSON", false).action(async (path, opts) => {
|
|
97745
98271
|
try {
|
|
97746
|
-
const repoPath =
|
|
98272
|
+
const repoPath = resolve4(path ?? process.cwd());
|
|
97747
98273
|
const snapshot = discoverRepo({
|
|
97748
98274
|
repoPath,
|
|
97749
98275
|
refresh: opts.refresh,
|
|
@@ -97809,7 +98335,7 @@ repoCmd.command("run [path]").description("Run discovered Playwright tests nativ
|
|
|
97809
98335
|
repoCmd.command("cache [path]").description("Manage discovery cache").option("--clear", "Clear discovery cache", false).option("--status", "Show cache status", false).action((path, opts) => {
|
|
97810
98336
|
try {
|
|
97811
98337
|
if (opts.clear) {
|
|
97812
|
-
const repoPath2 = path ?
|
|
98338
|
+
const repoPath2 = path ? resolve4(path) : undefined;
|
|
97813
98339
|
clearDiscoveryCache(repoPath2);
|
|
97814
98340
|
if (repoPath2) {
|
|
97815
98341
|
log(chalk6.green("Discovery cache cleared for this repo."));
|
|
@@ -97819,7 +98345,7 @@ repoCmd.command("cache [path]").description("Manage discovery cache").option("--
|
|
|
97819
98345
|
return;
|
|
97820
98346
|
}
|
|
97821
98347
|
if (opts.status) {
|
|
97822
|
-
const repoPath2 =
|
|
98348
|
+
const repoPath2 = resolve4(path ?? process.cwd());
|
|
97823
98349
|
const info2 = getDiscoveryCacheInfo(repoPath2);
|
|
97824
98350
|
if (!info2) {
|
|
97825
98351
|
log(chalk6.dim("No discovery cache for this repo."));
|
|
@@ -97833,7 +98359,7 @@ repoCmd.command("cache [path]").description("Manage discovery cache").option("--
|
|
|
97833
98359
|
log("");
|
|
97834
98360
|
return;
|
|
97835
98361
|
}
|
|
97836
|
-
const repoPath =
|
|
98362
|
+
const repoPath = resolve4(path ?? process.cwd());
|
|
97837
98363
|
const info = getDiscoveryCacheInfo(repoPath);
|
|
97838
98364
|
if (!info) {
|
|
97839
98365
|
log(chalk6.dim("No discovery cache. Run 'testers repo discover' to create one."));
|
|
@@ -97922,8 +98448,8 @@ sessionCmd.command("show <id>").description("Show details of a recorded session"
|
|
|
97922
98448
|
});
|
|
97923
98449
|
sessionCmd.command("import <file>").description("Import a session JSON file exported from the Chrome extension").action(async (file2) => {
|
|
97924
98450
|
try {
|
|
97925
|
-
const { readFileSync:
|
|
97926
|
-
const raw =
|
|
98451
|
+
const { readFileSync: readFileSync11 } = await import("fs");
|
|
98452
|
+
const raw = readFileSync11(file2, "utf-8");
|
|
97927
98453
|
const data = JSON.parse(raw);
|
|
97928
98454
|
const items = Array.isArray(data) ? data : [data];
|
|
97929
98455
|
const { createSession: createSession2 } = await Promise.resolve().then(() => (init_sessions(), exports_sessions));
|
|
@@ -98163,7 +98689,7 @@ program2.command("daemon").description("Start the scheduler daemon").option("--i
|
|
|
98163
98689
|
} catch (err) {
|
|
98164
98690
|
logError(chalk6.red(`Daemon error: ${err instanceof Error ? err.message : String(err)}`));
|
|
98165
98691
|
}
|
|
98166
|
-
await new Promise((
|
|
98692
|
+
await new Promise((resolve5) => setTimeout(resolve5, intervalMs));
|
|
98167
98693
|
}
|
|
98168
98694
|
};
|
|
98169
98695
|
process.on("SIGINT", () => {
|
|
@@ -98192,12 +98718,12 @@ program2.command("ci [provider]").description("Print or write a CI workflow (def
|
|
|
98192
98718
|
}
|
|
98193
98719
|
const workflow = generateGitHubActionsWorkflow();
|
|
98194
98720
|
if (opts.output) {
|
|
98195
|
-
const outPath =
|
|
98721
|
+
const outPath = resolve4(opts.output);
|
|
98196
98722
|
const outDir = outPath.replace(/\/[^/]*$/, "");
|
|
98197
98723
|
if (outDir && !existsSync17(outDir)) {
|
|
98198
98724
|
mkdirSync14(outDir, { recursive: true });
|
|
98199
98725
|
}
|
|
98200
|
-
|
|
98726
|
+
writeFileSync8(outPath, workflow, "utf-8");
|
|
98201
98727
|
log(chalk6.green(`Workflow written to ${outPath}`));
|
|
98202
98728
|
return;
|
|
98203
98729
|
}
|
|
@@ -98228,12 +98754,12 @@ program2.command("init").description("Initialize a new testing project").option(
|
|
|
98228
98754
|
log(` ${chalk6.dim(s2.shortId)} ${s2.name} ${chalk6.dim(`[${s2.tags.join(", ")}]`)}`);
|
|
98229
98755
|
}
|
|
98230
98756
|
if (opts.ci === "github") {
|
|
98231
|
-
const workflowDir =
|
|
98757
|
+
const workflowDir = join20(process.cwd(), ".github", "workflows");
|
|
98232
98758
|
if (!existsSync17(workflowDir)) {
|
|
98233
98759
|
mkdirSync14(workflowDir, { recursive: true });
|
|
98234
98760
|
}
|
|
98235
|
-
const workflowPath =
|
|
98236
|
-
|
|
98761
|
+
const workflowPath = join20(workflowDir, "testers.yml");
|
|
98762
|
+
writeFileSync8(workflowPath, generateGitHubActionsWorkflow(), "utf-8");
|
|
98237
98763
|
log(` CI: ${chalk6.green("GitHub Actions workflow written to .github/workflows/testers.yml")}`);
|
|
98238
98764
|
} else if (opts.ci) {
|
|
98239
98765
|
log(chalk6.yellow(` Unknown CI provider: ${opts.ci}. Supported: github`));
|
|
@@ -98242,7 +98768,7 @@ program2.command("init").description("Initialize a new testing project").option(
|
|
|
98242
98768
|
if (opts.yes)
|
|
98243
98769
|
return;
|
|
98244
98770
|
const rl2 = createInterface({ input: process.stdin, output: process.stdout });
|
|
98245
|
-
const ask = (q2) => new Promise((
|
|
98771
|
+
const ask = (q2) => new Promise((resolve5) => rl2.question(q2, resolve5));
|
|
98246
98772
|
try {
|
|
98247
98773
|
const envAnswer = await ask(" Would you like to configure environments? [y/N] ");
|
|
98248
98774
|
if (envAnswer.trim().toLowerCase() === "y") {
|
|
@@ -98427,8 +98953,8 @@ program2.command("report [run-id]").description("Generate HTML test report or co
|
|
|
98427
98953
|
format
|
|
98428
98954
|
});
|
|
98429
98955
|
if (opts.output && opts.output !== "report.html") {
|
|
98430
|
-
|
|
98431
|
-
const absPath2 =
|
|
98956
|
+
writeFileSync8(opts.output, content, "utf-8");
|
|
98957
|
+
const absPath2 = resolve4(opts.output);
|
|
98432
98958
|
log(chalk6.green(`Compliance report written to ${absPath2}`));
|
|
98433
98959
|
} else {
|
|
98434
98960
|
log(content);
|
|
@@ -98441,8 +98967,8 @@ program2.command("report [run-id]").description("Generate HTML test report or co
|
|
|
98441
98967
|
} else {
|
|
98442
98968
|
html = generateHtmlReport(runId);
|
|
98443
98969
|
}
|
|
98444
|
-
|
|
98445
|
-
const absPath =
|
|
98970
|
+
writeFileSync8(opts.output, html, "utf-8");
|
|
98971
|
+
const absPath = resolve4(opts.output);
|
|
98446
98972
|
log(chalk6.green(`Report generated: ${absPath}`));
|
|
98447
98973
|
if (opts.open) {
|
|
98448
98974
|
const openCmd = process.platform === "darwin" ? "open" : "xdg-open";
|
|
@@ -99251,7 +99777,7 @@ program2.command("doctor").description("Check system setup and configuration").a
|
|
|
99251
99777
|
log(chalk6.red("\u2717") + " ANTHROPIC_API_KEY is not set (required for AI-powered tests)");
|
|
99252
99778
|
allPassed = false;
|
|
99253
99779
|
}
|
|
99254
|
-
const dbPath =
|
|
99780
|
+
const dbPath = join20(getTestersDir(), "testers.db");
|
|
99255
99781
|
try {
|
|
99256
99782
|
const { Database: Database5 } = await import("bun:sqlite");
|
|
99257
99783
|
const db2 = new Database5(dbPath, { create: true });
|
|
@@ -99303,7 +99829,7 @@ program2.command("serve").description("Start the Open Testers web dashboard").op
|
|
|
99303
99829
|
try {
|
|
99304
99830
|
const port = parseInt(opts.port, 10);
|
|
99305
99831
|
const url2 = `http://localhost:${port}`;
|
|
99306
|
-
const serverBin =
|
|
99832
|
+
const serverBin = join20(resolve4(process.execPath, ".."), "..", "dist", "server", "index.js");
|
|
99307
99833
|
const { join: pathJoin, resolve: pathResolve, dirname: dirname7 } = await import("path");
|
|
99308
99834
|
const { fileURLToPath: fileURLToPath2 } = await import("url");
|
|
99309
99835
|
const serverPath = pathJoin(dirname7(fileURLToPath2(import.meta.url)), "..", "server", "index.js");
|
|
@@ -99719,7 +100245,7 @@ workflowCmd.command("create <name>").description("Save a reusable testing workfl
|
|
|
99719
100245
|
}, []).option("--priority <level>", "Scenario priority").option("--persona <ids>", "Comma-separated persona IDs").option("--goal <prompt>", "Goal prompt for the agentic testing loop").option("--success <criteria>", "Success criteria (repeatable)", (val, acc) => {
|
|
99720
100246
|
acc.push(val);
|
|
99721
100247
|
return acc;
|
|
99722
|
-
}, []).option("--max-iterations <n>", "Goal-loop iteration cap", "10").option("--target <target>", "Execution target: local or
|
|
100248
|
+
}, []).option("--max-iterations <n>", "Goal-loop iteration cap", "10").option("--target <target>", "Execution target: local or sandbox", "local").option("--sandbox-provider <provider>", "Sandbox provider: e2b, daytona, or modal").option("--sandbox-image <image>", "Sandbox image/template").option("--sandbox-remote-dir <path>", "Remote working directory for sandbox runs").option("--sandbox-cleanup <mode>", "Sandbox cleanup mode: delete, stop, or keep", "delete").option("--sandbox-setup-command <command>", "Shell command to run before testers in the sandbox").option("--sandbox-package <spec>", "Package spec to execute in the sandbox", "@hasna/testers").option("--e2b-template <name>", "Legacy alias for --sandbox-image").option("--timeout <ms>", "Workflow timeout").option("--json", "Output as JSON", false).action((name21, opts) => {
|
|
99723
100249
|
try {
|
|
99724
100250
|
const workflow = createTestingWorkflow({
|
|
99725
100251
|
name: name21,
|
|
@@ -99738,9 +100264,12 @@ workflowCmd.command("create <name>").description("Save a reusable testing workfl
|
|
|
99738
100264
|
} : null,
|
|
99739
100265
|
execution: {
|
|
99740
100266
|
target: opts.target,
|
|
99741
|
-
|
|
99742
|
-
|
|
99743
|
-
|
|
100267
|
+
provider: opts.sandboxProvider ?? (opts.target === "connector:e2b" ? "e2b" : undefined),
|
|
100268
|
+
sandboxImage: opts.sandboxImage ?? opts.e2bTemplate,
|
|
100269
|
+
sandboxRemoteDir: opts.sandboxRemoteDir,
|
|
100270
|
+
sandboxCleanup: opts.sandboxCleanup,
|
|
100271
|
+
setupCommand: opts.sandboxSetupCommand,
|
|
100272
|
+
packageSpec: opts.sandboxPackage,
|
|
99744
100273
|
timeoutMs: opts.timeout ? parseInt(opts.timeout, 10) : undefined
|
|
99745
100274
|
}
|
|
99746
100275
|
});
|
|
@@ -99772,7 +100301,7 @@ workflowCmd.command("list").description("List saved testing workflows").option("
|
|
|
99772
100301
|
log(chalk6.bold(" Testing Workflows"));
|
|
99773
100302
|
log("");
|
|
99774
100303
|
for (const workflow of workflows) {
|
|
99775
|
-
const target = workflow.execution.target === "
|
|
100304
|
+
const target = workflow.execution.target === "sandbox" ? chalk6.cyan(`sandbox${workflow.execution.provider ? `:${workflow.execution.provider}` : ""}`) : chalk6.green("local");
|
|
99776
100305
|
log(` ${chalk6.dim(workflow.id.slice(0, 8))} ${workflow.name} ${target} ${chalk6.dim(workflow.personaIds.length ? `${workflow.personaIds.length} personas` : "no personas")}`);
|
|
99777
100306
|
}
|
|
99778
100307
|
log("");
|
|
@@ -99984,7 +100513,7 @@ personaCmd.command("delete <id>").description("Delete a persona").option("-y, --
|
|
|
99984
100513
|
}
|
|
99985
100514
|
if (!opts.yes) {
|
|
99986
100515
|
process.stdout.write(chalk6.yellow(`Delete persona ${persona.shortId} "${persona.name}"? [y/N] `));
|
|
99987
|
-
const answer = await new Promise((
|
|
100516
|
+
const answer = await new Promise((resolve5) => {
|
|
99988
100517
|
let buf = "";
|
|
99989
100518
|
process.stdin.setRawMode?.(true);
|
|
99990
100519
|
process.stdin.resume();
|
|
@@ -99994,7 +100523,7 @@ personaCmd.command("delete <id>").description("Delete a persona").option("-y, --
|
|
|
99994
100523
|
process.stdin.pause();
|
|
99995
100524
|
process.stdout.write(`
|
|
99996
100525
|
`);
|
|
99997
|
-
|
|
100526
|
+
resolve5(buf);
|
|
99998
100527
|
});
|
|
99999
100528
|
});
|
|
100000
100529
|
if (answer !== "y" && answer !== "yes") {
|
|
@@ -100155,7 +100684,7 @@ evalCmd.command("rag <url>").description("Run RAG quality evaluation \u2014 fait
|
|
|
100155
100684
|
let ragTestCases = [];
|
|
100156
100685
|
if (opts.docs) {
|
|
100157
100686
|
try {
|
|
100158
|
-
const raw =
|
|
100687
|
+
const raw = readFileSync10(opts.docs, "utf-8");
|
|
100159
100688
|
ragTestCases = JSON.parse(raw);
|
|
100160
100689
|
} catch {
|
|
100161
100690
|
logError(chalk6.red(`Failed to read docs file: ${opts.docs}`));
|
|
@@ -100245,9 +100774,9 @@ Created golden answer check ${chalk6.bold(golden2.shortId)}`));
|
|
|
100245
100774
|
}
|
|
100246
100775
|
const ask = (prompt) => {
|
|
100247
100776
|
const rl2 = createInterface({ input: process.stdin, output: process.stdout });
|
|
100248
|
-
return new Promise((
|
|
100777
|
+
return new Promise((resolve5) => rl2.question(prompt, (ans) => {
|
|
100249
100778
|
rl2.close();
|
|
100250
|
-
|
|
100779
|
+
resolve5(ans.trim());
|
|
100251
100780
|
}));
|
|
100252
100781
|
};
|
|
100253
100782
|
const question = await ask("Question (what this endpoint should answer): ");
|
|
@@ -100408,9 +100937,9 @@ program2.command("run-many <url>").description("Run scenarios \xD7 personas matr
|
|
|
100408
100937
|
});
|
|
100409
100938
|
program2.command("run-script <file>").description("Run a hybrid test script (.ts) that exports an array of HybridScenario objects").option("--url <url>", "Base URL to run against").option("--json", "Output as JSON", false).action(async (file2, opts) => {
|
|
100410
100939
|
try {
|
|
100411
|
-
const { resolve:
|
|
100940
|
+
const { resolve: resolve5 } = await import("path");
|
|
100412
100941
|
const { runHybridScenario: runHybridScenario2 } = await Promise.resolve().then(() => (init_hybrid_runner(), exports_hybrid_runner));
|
|
100413
|
-
const scriptPath =
|
|
100942
|
+
const scriptPath = resolve5(process.cwd(), file2);
|
|
100414
100943
|
const mod = await import(scriptPath);
|
|
100415
100944
|
const scenarios = mod.scenarios ?? mod.default ?? [];
|
|
100416
100945
|
if (!Array.isArray(scenarios) || scenarios.length === 0) {
|