@hasna/testers 0.0.34 → 0.0.36
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 +1224 -1063
- 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 +989 -214
- package/dist/lib/ai-client.d.ts +10 -2
- 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/browser-compat.d.ts +14 -0
- package/dist/lib/browser-compat.d.ts.map +1 -0
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/crawl-and-generate.d.ts +1 -1
- package/dist/lib/crawl-and-generate.d.ts.map +1 -1
- package/dist/lib/generator.d.ts.map +1 -1
- package/dist/lib/healer.d.ts.map +1 -1
- package/dist/lib/hybrid-runner.d.ts.map +1 -1
- package/dist/lib/judge.d.ts +3 -3
- package/dist/lib/judge.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 +30 -1
- package/dist/lib/runner.d.ts.map +1 -1
- package/dist/lib/session-converter.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 -1
- package/dist/mcp/index.js +1016 -851
- 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 +942 -609
- package/dist/types/index.d.ts +23 -3
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +7 -6
package/dist/index.js
CHANGED
|
@@ -17,6 +17,72 @@ var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
|
17
17
|
var __require = import.meta.require;
|
|
18
18
|
|
|
19
19
|
// src/types/index.ts
|
|
20
|
+
function isRecord(value) {
|
|
21
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
22
|
+
}
|
|
23
|
+
function stringValue(value) {
|
|
24
|
+
return typeof value === "string" && value.trim() ? value : undefined;
|
|
25
|
+
}
|
|
26
|
+
function numberValue(value) {
|
|
27
|
+
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
28
|
+
}
|
|
29
|
+
function stringMap(value) {
|
|
30
|
+
if (!isRecord(value))
|
|
31
|
+
return;
|
|
32
|
+
const entries = Object.entries(value).filter((entry) => typeof entry[1] === "string");
|
|
33
|
+
return entries.length > 0 ? Object.fromEntries(entries) : undefined;
|
|
34
|
+
}
|
|
35
|
+
function cleanupValue(value) {
|
|
36
|
+
if (value === "delete" || value === "stop" || value === "keep")
|
|
37
|
+
return value;
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
function workflowExecutionFromValue(value) {
|
|
41
|
+
const input = isRecord(value) ? value : {};
|
|
42
|
+
const rawTarget = stringValue(input["target"]) ?? "local";
|
|
43
|
+
if (rawTarget === "local") {
|
|
44
|
+
const timeoutMs2 = numberValue(input["timeoutMs"]);
|
|
45
|
+
return timeoutMs2 === undefined ? { target: "local" } : { target: "local", timeoutMs: timeoutMs2 };
|
|
46
|
+
}
|
|
47
|
+
if (rawTarget !== "sandbox" && rawTarget !== "connector:e2b") {
|
|
48
|
+
throw new Error(`Unsupported workflow execution target: ${rawTarget}`);
|
|
49
|
+
}
|
|
50
|
+
const provider = rawTarget === "connector:e2b" ? "e2b" : stringValue(input["provider"]) ?? stringValue(input["connector"]);
|
|
51
|
+
const sandboxImage = stringValue(input["sandboxImage"]) ?? stringValue(input["sandboxTemplate"]);
|
|
52
|
+
const sandboxRemoteDir = stringValue(input["sandboxRemoteDir"]);
|
|
53
|
+
const sandboxCleanup = cleanupValue(input["sandboxCleanup"]);
|
|
54
|
+
const setupCommand = stringValue(input["setupCommand"]);
|
|
55
|
+
const packageSpec = stringValue(input["packageSpec"]);
|
|
56
|
+
const timeoutMs = numberValue(input["timeoutMs"]);
|
|
57
|
+
const env = stringMap(input["env"]);
|
|
58
|
+
return {
|
|
59
|
+
target: "sandbox",
|
|
60
|
+
...provider ? { provider } : {},
|
|
61
|
+
...sandboxImage ? { sandboxImage } : {},
|
|
62
|
+
...sandboxRemoteDir ? { sandboxRemoteDir } : {},
|
|
63
|
+
...sandboxCleanup ? { sandboxCleanup } : {},
|
|
64
|
+
...setupCommand ? { setupCommand } : {},
|
|
65
|
+
...packageSpec ? { packageSpec } : {},
|
|
66
|
+
...timeoutMs !== undefined ? { timeoutMs } : {},
|
|
67
|
+
...env ? { env } : {}
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
function workflowFromRow(row) {
|
|
71
|
+
return {
|
|
72
|
+
id: row.id,
|
|
73
|
+
projectId: row.project_id,
|
|
74
|
+
name: row.name,
|
|
75
|
+
description: row.description,
|
|
76
|
+
scenarioFilter: JSON.parse(row.scenario_filter || "{}"),
|
|
77
|
+
personaIds: JSON.parse(row.persona_ids || "[]"),
|
|
78
|
+
goal: row.goal ? JSON.parse(row.goal) : null,
|
|
79
|
+
execution: workflowExecutionFromValue(JSON.parse(row.execution || '{"target":"local"}')),
|
|
80
|
+
settings: JSON.parse(row.settings || "{}"),
|
|
81
|
+
enabled: row.enabled === 1,
|
|
82
|
+
createdAt: row.created_at,
|
|
83
|
+
updatedAt: row.updated_at
|
|
84
|
+
};
|
|
85
|
+
}
|
|
20
86
|
function projectFromRow(row) {
|
|
21
87
|
return {
|
|
22
88
|
id: row.id,
|
|
@@ -10456,7 +10522,7 @@ __export(exports_flows, {
|
|
|
10456
10522
|
addDependency: () => addDependency
|
|
10457
10523
|
});
|
|
10458
10524
|
function addDependency(scenarioId, dependsOn) {
|
|
10459
|
-
const
|
|
10525
|
+
const db3 = getDatabase();
|
|
10460
10526
|
const visited = new Set;
|
|
10461
10527
|
const queue = [dependsOn];
|
|
10462
10528
|
while (queue.length > 0) {
|
|
@@ -10467,37 +10533,37 @@ function addDependency(scenarioId, dependsOn) {
|
|
|
10467
10533
|
if (visited.has(current))
|
|
10468
10534
|
continue;
|
|
10469
10535
|
visited.add(current);
|
|
10470
|
-
const deps =
|
|
10536
|
+
const deps = db3.query("SELECT depends_on FROM scenario_dependencies WHERE scenario_id = ?").all(current);
|
|
10471
10537
|
for (const dep of deps) {
|
|
10472
10538
|
if (!visited.has(dep.depends_on)) {
|
|
10473
10539
|
queue.push(dep.depends_on);
|
|
10474
10540
|
}
|
|
10475
10541
|
}
|
|
10476
10542
|
}
|
|
10477
|
-
|
|
10543
|
+
db3.query("INSERT OR IGNORE INTO scenario_dependencies (scenario_id, depends_on) VALUES (?, ?)").run(scenarioId, dependsOn);
|
|
10478
10544
|
}
|
|
10479
10545
|
function removeDependency(scenarioId, dependsOn) {
|
|
10480
|
-
const
|
|
10481
|
-
const result =
|
|
10546
|
+
const db3 = getDatabase();
|
|
10547
|
+
const result = db3.query("DELETE FROM scenario_dependencies WHERE scenario_id = ? AND depends_on = ?").run(scenarioId, dependsOn);
|
|
10482
10548
|
return result.changes > 0;
|
|
10483
10549
|
}
|
|
10484
10550
|
function getDependencies(scenarioId) {
|
|
10485
|
-
const
|
|
10486
|
-
const rows =
|
|
10551
|
+
const db3 = getDatabase();
|
|
10552
|
+
const rows = db3.query("SELECT depends_on FROM scenario_dependencies WHERE scenario_id = ?").all(scenarioId);
|
|
10487
10553
|
return rows.map((r) => r.depends_on);
|
|
10488
10554
|
}
|
|
10489
10555
|
function getDependents(scenarioId) {
|
|
10490
|
-
const
|
|
10491
|
-
const rows =
|
|
10556
|
+
const db3 = getDatabase();
|
|
10557
|
+
const rows = db3.query("SELECT scenario_id FROM scenario_dependencies WHERE depends_on = ?").all(scenarioId);
|
|
10492
10558
|
return rows.map((r) => r.scenario_id);
|
|
10493
10559
|
}
|
|
10494
10560
|
function getTransitiveDependencies(scenarioId) {
|
|
10495
|
-
const
|
|
10561
|
+
const db3 = getDatabase();
|
|
10496
10562
|
const visited = new Set;
|
|
10497
10563
|
const queue = [scenarioId];
|
|
10498
10564
|
while (queue.length > 0) {
|
|
10499
10565
|
const current = queue.shift();
|
|
10500
|
-
const deps =
|
|
10566
|
+
const deps = db3.query("SELECT depends_on FROM scenario_dependencies WHERE scenario_id = ?").all(current);
|
|
10501
10567
|
for (const dep of deps) {
|
|
10502
10568
|
if (!visited.has(dep.depends_on)) {
|
|
10503
10569
|
visited.add(dep.depends_on);
|
|
@@ -10508,7 +10574,7 @@ function getTransitiveDependencies(scenarioId) {
|
|
|
10508
10574
|
return Array.from(visited);
|
|
10509
10575
|
}
|
|
10510
10576
|
function topologicalSort(scenarioIds) {
|
|
10511
|
-
const
|
|
10577
|
+
const db3 = getDatabase();
|
|
10512
10578
|
const idSet = new Set(scenarioIds);
|
|
10513
10579
|
const inDegree = new Map;
|
|
10514
10580
|
const dependents = new Map;
|
|
@@ -10517,7 +10583,7 @@ function topologicalSort(scenarioIds) {
|
|
|
10517
10583
|
dependents.set(id, []);
|
|
10518
10584
|
}
|
|
10519
10585
|
for (const id of scenarioIds) {
|
|
10520
|
-
const deps =
|
|
10586
|
+
const deps = db3.query("SELECT depends_on FROM scenario_dependencies WHERE scenario_id = ?").all(id);
|
|
10521
10587
|
for (const dep of deps) {
|
|
10522
10588
|
if (idSet.has(dep.depends_on)) {
|
|
10523
10589
|
inDegree.set(id, (inDegree.get(id) ?? 0) + 1);
|
|
@@ -10547,43 +10613,43 @@ function topologicalSort(scenarioIds) {
|
|
|
10547
10613
|
return sorted;
|
|
10548
10614
|
}
|
|
10549
10615
|
function createFlow(input) {
|
|
10550
|
-
const
|
|
10616
|
+
const db3 = getDatabase();
|
|
10551
10617
|
const id = uuid();
|
|
10552
10618
|
const timestamp = now();
|
|
10553
|
-
|
|
10619
|
+
db3.query(`
|
|
10554
10620
|
INSERT INTO flows (id, project_id, name, description, scenario_ids, created_at, updated_at)
|
|
10555
10621
|
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
10556
10622
|
`).run(id, input.projectId ?? null, input.name, input.description ?? null, JSON.stringify(input.scenarioIds), timestamp, timestamp);
|
|
10557
10623
|
return getFlow(id);
|
|
10558
10624
|
}
|
|
10559
10625
|
function getFlow(id) {
|
|
10560
|
-
const
|
|
10561
|
-
let row =
|
|
10626
|
+
const db3 = getDatabase();
|
|
10627
|
+
let row = db3.query("SELECT * FROM flows WHERE id = ?").get(id);
|
|
10562
10628
|
if (row)
|
|
10563
10629
|
return flowFromRow(row);
|
|
10564
10630
|
const fullId = resolvePartialId("flows", id);
|
|
10565
10631
|
if (fullId) {
|
|
10566
|
-
row =
|
|
10632
|
+
row = db3.query("SELECT * FROM flows WHERE id = ?").get(fullId);
|
|
10567
10633
|
if (row)
|
|
10568
10634
|
return flowFromRow(row);
|
|
10569
10635
|
}
|
|
10570
10636
|
return null;
|
|
10571
10637
|
}
|
|
10572
10638
|
function listFlows(projectId) {
|
|
10573
|
-
const
|
|
10639
|
+
const db3 = getDatabase();
|
|
10574
10640
|
if (projectId) {
|
|
10575
|
-
const rows2 =
|
|
10641
|
+
const rows2 = db3.query("SELECT * FROM flows WHERE project_id = ? ORDER BY created_at DESC").all(projectId);
|
|
10576
10642
|
return rows2.map(flowFromRow);
|
|
10577
10643
|
}
|
|
10578
|
-
const rows =
|
|
10644
|
+
const rows = db3.query("SELECT * FROM flows ORDER BY created_at DESC").all();
|
|
10579
10645
|
return rows.map(flowFromRow);
|
|
10580
10646
|
}
|
|
10581
10647
|
function deleteFlow(id) {
|
|
10582
|
-
const
|
|
10648
|
+
const db3 = getDatabase();
|
|
10583
10649
|
const flow = getFlow(id);
|
|
10584
10650
|
if (!flow)
|
|
10585
10651
|
return false;
|
|
10586
|
-
const result =
|
|
10652
|
+
const result = db3.query("DELETE FROM flows WHERE id = ?").run(flow.id);
|
|
10587
10653
|
return result.changes > 0;
|
|
10588
10654
|
}
|
|
10589
10655
|
var init_flows = __esm(() => {
|
|
@@ -10669,6 +10735,10 @@ function loadConfig() {
|
|
|
10669
10735
|
if (envApiKey) {
|
|
10670
10736
|
config.anthropicApiKey = envApiKey;
|
|
10671
10737
|
}
|
|
10738
|
+
const envSelfHeal = process.env["TESTERS_SELF_HEAL"];
|
|
10739
|
+
if (envSelfHeal !== undefined) {
|
|
10740
|
+
config.selfHeal = ["1", "true", "yes", "on"].includes(envSelfHeal.toLowerCase());
|
|
10741
|
+
}
|
|
10672
10742
|
return config;
|
|
10673
10743
|
}
|
|
10674
10744
|
function resolveModel(nameOrId) {
|
|
@@ -11586,12 +11656,11 @@ Original selector that failed: "${request.failedSelector}"
|
|
|
11586
11656
|
Please identify the correct selector from the screenshot.`;
|
|
11587
11657
|
let rawResponse = "";
|
|
11588
11658
|
try {
|
|
11589
|
-
if (provider
|
|
11590
|
-
const
|
|
11591
|
-
const apiKey = provider === "openai" ? process.env["OPENAI_API_KEY"] ?? "" : process.env["GOOGLE_API_KEY"] ?? "";
|
|
11659
|
+
if (provider !== "anthropic") {
|
|
11660
|
+
const compat = createOpenAICompatibleConfig(provider);
|
|
11592
11661
|
const resp = await callOpenAICompatible({
|
|
11593
|
-
baseUrl,
|
|
11594
|
-
apiKey,
|
|
11662
|
+
baseUrl: compat.baseUrl,
|
|
11663
|
+
apiKey: compat.apiKey,
|
|
11595
11664
|
model,
|
|
11596
11665
|
system: HEAL_SYSTEM,
|
|
11597
11666
|
messages: [{ role: "user", content: userMessage }],
|
|
@@ -12092,7 +12161,6 @@ async function executeTool(page, screenshotter, toolName, toolInput, context) {
|
|
|
12092
12161
|
const assertionType = toolInput.assertion_type;
|
|
12093
12162
|
const selector = toolInput.selector;
|
|
12094
12163
|
const expected = toolInput.expected;
|
|
12095
|
-
const sessionId = context.sessionId ?? "default";
|
|
12096
12164
|
switch (assertionType) {
|
|
12097
12165
|
case "element_exists": {
|
|
12098
12166
|
if (!selector)
|
|
@@ -12157,7 +12225,6 @@ async function executeTool(page, screenshotter, toolName, toolInput, context) {
|
|
|
12157
12225
|
case "browser_intercept": {
|
|
12158
12226
|
const action = toolInput.action;
|
|
12159
12227
|
const pattern = toolInput.pattern;
|
|
12160
|
-
const interceptAction = toolInput.intercept_action;
|
|
12161
12228
|
const statusCode = toolInput.status_code;
|
|
12162
12229
|
const body = toolInput.body;
|
|
12163
12230
|
const sessionId = context.sessionId ?? "default";
|
|
@@ -12234,7 +12301,28 @@ ${JSON.stringify(har, null, 2)}` };
|
|
|
12234
12301
|
}
|
|
12235
12302
|
case "browser_a11y": {
|
|
12236
12303
|
const level = toolInput.level ?? "AA";
|
|
12237
|
-
const snapshot = await page.
|
|
12304
|
+
const snapshot = await page.evaluate(() => {
|
|
12305
|
+
function readRole(el) {
|
|
12306
|
+
return el.getAttribute("role") ?? el.tagName.toLowerCase();
|
|
12307
|
+
}
|
|
12308
|
+
function readName(el) {
|
|
12309
|
+
const labelledBy = el.getAttribute("aria-labelledby");
|
|
12310
|
+
if (labelledBy) {
|
|
12311
|
+
const labelledText = labelledBy.split(/\s+/).map((id) => document.getElementById(id)?.textContent?.trim()).filter(Boolean).join(" ");
|
|
12312
|
+
if (labelledText)
|
|
12313
|
+
return labelledText;
|
|
12314
|
+
}
|
|
12315
|
+
return el.getAttribute("aria-label") ?? el.getAttribute("alt") ?? el.textContent?.trim() ?? "";
|
|
12316
|
+
}
|
|
12317
|
+
function walk(el) {
|
|
12318
|
+
return {
|
|
12319
|
+
role: readRole(el),
|
|
12320
|
+
name: readName(el),
|
|
12321
|
+
children: Array.from(el.children).map((child) => walk(child))
|
|
12322
|
+
};
|
|
12323
|
+
}
|
|
12324
|
+
return document.body ? walk(document.body) : null;
|
|
12325
|
+
});
|
|
12238
12326
|
if (!snapshot)
|
|
12239
12327
|
return { result: "Error: could not capture accessibility tree" };
|
|
12240
12328
|
const issues = [];
|
|
@@ -12276,6 +12364,38 @@ ${filtered.join(`
|
|
|
12276
12364
|
return { result: `Error executing ${toolName}: ${message}` };
|
|
12277
12365
|
}
|
|
12278
12366
|
}
|
|
12367
|
+
function resolveStartUrl(baseUrl, targetPath) {
|
|
12368
|
+
try {
|
|
12369
|
+
return new URL(targetPath, baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`).toString();
|
|
12370
|
+
} catch {
|
|
12371
|
+
return `${baseUrl.replace(/\/+$/, "")}/${targetPath.replace(/^\/+/, "")}`;
|
|
12372
|
+
}
|
|
12373
|
+
}
|
|
12374
|
+
function buildScenarioUserMessage(scenario, baseUrl) {
|
|
12375
|
+
const userParts = [
|
|
12376
|
+
`**Scenario:** ${scenario.name}`,
|
|
12377
|
+
`**Description:** ${scenario.description}`
|
|
12378
|
+
];
|
|
12379
|
+
if (baseUrl) {
|
|
12380
|
+
const normalizedBaseUrl = baseUrl.replace(/\/+$/, "");
|
|
12381
|
+
userParts.push(`**Base URL:** ${normalizedBaseUrl}`);
|
|
12382
|
+
if (scenario.targetPath) {
|
|
12383
|
+
userParts.push(`**Start URL:** ${resolveStartUrl(normalizedBaseUrl, scenario.targetPath)}`);
|
|
12384
|
+
}
|
|
12385
|
+
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.");
|
|
12386
|
+
}
|
|
12387
|
+
if (scenario.targetPath) {
|
|
12388
|
+
userParts.push(`**Target Path:** ${scenario.targetPath}`);
|
|
12389
|
+
}
|
|
12390
|
+
if (scenario.steps.length > 0) {
|
|
12391
|
+
userParts.push("**Steps:**");
|
|
12392
|
+
for (let i = 0;i < scenario.steps.length; i++) {
|
|
12393
|
+
userParts.push(`${i + 1}. ${scenario.steps[i]}`);
|
|
12394
|
+
}
|
|
12395
|
+
}
|
|
12396
|
+
return userParts.join(`
|
|
12397
|
+
`);
|
|
12398
|
+
}
|
|
12279
12399
|
async function runAgentLoop(options) {
|
|
12280
12400
|
const {
|
|
12281
12401
|
client,
|
|
@@ -12285,6 +12405,7 @@ async function runAgentLoop(options) {
|
|
|
12285
12405
|
model,
|
|
12286
12406
|
runId,
|
|
12287
12407
|
sessionId,
|
|
12408
|
+
baseUrl,
|
|
12288
12409
|
maxTurns = 30,
|
|
12289
12410
|
onStep,
|
|
12290
12411
|
persona,
|
|
@@ -12332,21 +12453,7 @@ Instructions: ${persona.instructions}` : "",
|
|
|
12332
12453
|
"- Verify both positive and negative states"
|
|
12333
12454
|
].join(`
|
|
12334
12455
|
`) + personaSection;
|
|
12335
|
-
const
|
|
12336
|
-
`**Scenario:** ${scenario.name}`,
|
|
12337
|
-
`**Description:** ${scenario.description}`
|
|
12338
|
-
];
|
|
12339
|
-
if (scenario.targetPath) {
|
|
12340
|
-
userParts.push(`**Target Path:** ${scenario.targetPath}`);
|
|
12341
|
-
}
|
|
12342
|
-
if (scenario.steps.length > 0) {
|
|
12343
|
-
userParts.push("**Steps:**");
|
|
12344
|
-
for (let i = 0;i < scenario.steps.length; i++) {
|
|
12345
|
-
userParts.push(`${i + 1}. ${scenario.steps[i]}`);
|
|
12346
|
-
}
|
|
12347
|
-
}
|
|
12348
|
-
const userMessage = userParts.join(`
|
|
12349
|
-
`);
|
|
12456
|
+
const userMessage = buildScenarioUserMessage(scenario, baseUrl);
|
|
12350
12457
|
const screenshots = [];
|
|
12351
12458
|
let tokensUsed = 0;
|
|
12352
12459
|
let stepNumber = 0;
|
|
@@ -12409,7 +12516,7 @@ Instructions: ${persona.instructions}` : "",
|
|
|
12409
12516
|
if (onStep) {
|
|
12410
12517
|
onStep({ type: "tool_call", toolName: toolBlock.name, toolInput, stepNumber });
|
|
12411
12518
|
}
|
|
12412
|
-
const execResult = await executeTool(page, screenshotter, toolBlock.name, toolInput, { runId, scenarioSlug, stepNumber, sessionId, a11y });
|
|
12519
|
+
const execResult = await executeTool(page, screenshotter, toolBlock.name, toolInput, { runId, scenarioSlug, stepNumber, sessionId: sessionId ?? runId, a11y });
|
|
12413
12520
|
if (onStep) {
|
|
12414
12521
|
onStep({ type: "tool_result", toolName: toolBlock.name, toolResult: execResult.result, stepNumber });
|
|
12415
12522
|
}
|
|
@@ -12460,10 +12567,17 @@ function detectProvider(model) {
|
|
|
12460
12567
|
return "openai";
|
|
12461
12568
|
if (model.startsWith("gemini-"))
|
|
12462
12569
|
return "google";
|
|
12570
|
+
if (model.startsWith("glm-") || model.startsWith("zai/") || model.startsWith("zai-"))
|
|
12571
|
+
return "zai";
|
|
12463
12572
|
if (model.startsWith("llama-") || model.startsWith("qwen-") || model.includes("cerebras"))
|
|
12464
12573
|
return "cerebras";
|
|
12465
12574
|
return "anthropic";
|
|
12466
12575
|
}
|
|
12576
|
+
function resolveProviderApiKeyForModel(model, explicitApiKey, configuredAnthropicApiKey) {
|
|
12577
|
+
if (explicitApiKey)
|
|
12578
|
+
return explicitApiKey;
|
|
12579
|
+
return detectProvider(model) === "anthropic" ? configuredAnthropicApiKey : undefined;
|
|
12580
|
+
}
|
|
12467
12581
|
function createClient(apiKey) {
|
|
12468
12582
|
const key = apiKey ?? process.env["ANTHROPIC_API_KEY"];
|
|
12469
12583
|
if (!key) {
|
|
@@ -12541,26 +12655,34 @@ async function callOpenAICompatible(options) {
|
|
|
12541
12655
|
const usage = { input_tokens: data.usage?.prompt_tokens ?? 0, output_tokens: data.usage?.completion_tokens ?? 0 };
|
|
12542
12656
|
return { content, stop_reason: stopReason, usage };
|
|
12543
12657
|
}
|
|
12544
|
-
function
|
|
12545
|
-
const provider = detectProvider(model);
|
|
12658
|
+
function createOpenAICompatibleConfig(provider, apiKey) {
|
|
12546
12659
|
if (provider === "openai") {
|
|
12547
|
-
const
|
|
12548
|
-
if (!
|
|
12660
|
+
const key2 = apiKey ?? process.env["OPENAI_API_KEY"];
|
|
12661
|
+
if (!key2)
|
|
12549
12662
|
throw new AIClientError("No OpenAI API key. Set OPENAI_API_KEY or pass it explicitly.");
|
|
12550
|
-
return { provider: "openai", baseUrl: "https://api.openai.com/v1", apiKey:
|
|
12663
|
+
return { provider: "openai", baseUrl: "https://api.openai.com/v1", apiKey: key2 };
|
|
12551
12664
|
}
|
|
12552
12665
|
if (provider === "google") {
|
|
12553
|
-
const
|
|
12554
|
-
if (!
|
|
12666
|
+
const key2 = apiKey ?? process.env["GOOGLE_API_KEY"];
|
|
12667
|
+
if (!key2)
|
|
12555
12668
|
throw new AIClientError("No Google API key. Set GOOGLE_API_KEY or pass it explicitly.");
|
|
12556
|
-
return { provider: "google", baseUrl: "https://generativelanguage.googleapis.com/v1beta/openai", apiKey:
|
|
12669
|
+
return { provider: "google", baseUrl: "https://generativelanguage.googleapis.com/v1beta/openai", apiKey: key2 };
|
|
12557
12670
|
}
|
|
12558
12671
|
if (provider === "cerebras") {
|
|
12559
|
-
const
|
|
12560
|
-
if (!
|
|
12672
|
+
const key2 = apiKey ?? process.env["CEREBRAS_API_KEY"];
|
|
12673
|
+
if (!key2)
|
|
12561
12674
|
throw new AIClientError("No Cerebras API key. Set CEREBRAS_API_KEY or pass it explicitly.");
|
|
12562
|
-
return { provider: "cerebras", baseUrl: "https://api.cerebras.ai/v1", apiKey:
|
|
12675
|
+
return { provider: "cerebras", baseUrl: "https://api.cerebras.ai/v1", apiKey: key2 };
|
|
12563
12676
|
}
|
|
12677
|
+
const key = apiKey ?? process.env["ZAI_API_KEY"];
|
|
12678
|
+
if (!key)
|
|
12679
|
+
throw new AIClientError("No Z.AI API key. Set ZAI_API_KEY or pass it explicitly.");
|
|
12680
|
+
return { provider: "zai", baseUrl: "https://api.z.ai/api/paas/v4", apiKey: key };
|
|
12681
|
+
}
|
|
12682
|
+
function createClientForModel(model, apiKey) {
|
|
12683
|
+
const provider = detectProvider(model);
|
|
12684
|
+
if (provider !== "anthropic")
|
|
12685
|
+
return createOpenAICompatibleConfig(provider, apiKey);
|
|
12564
12686
|
return createClient(apiKey);
|
|
12565
12687
|
}
|
|
12566
12688
|
var activeHARs, activeCoverage, BROWSER_TOOLS;
|
|
@@ -13608,6 +13730,127 @@ function updateLastRun(id, runId, nextRunAt) {
|
|
|
13608
13730
|
UPDATE schedules SET last_run_id = ?, last_run_at = ?, next_run_at = ?, updated_at = ? WHERE id = ?
|
|
13609
13731
|
`).run(runId, timestamp, nextRunAt, timestamp, id);
|
|
13610
13732
|
}
|
|
13733
|
+
// src/db/workflows.ts
|
|
13734
|
+
init_types();
|
|
13735
|
+
init_database();
|
|
13736
|
+
var DEFAULT_EXECUTION = { target: "local" };
|
|
13737
|
+
function normalizeGoal(input) {
|
|
13738
|
+
if (!input)
|
|
13739
|
+
return null;
|
|
13740
|
+
const prompt = input.prompt?.trim();
|
|
13741
|
+
if (!prompt)
|
|
13742
|
+
return null;
|
|
13743
|
+
return {
|
|
13744
|
+
prompt,
|
|
13745
|
+
successCriteria: input.successCriteria ?? [],
|
|
13746
|
+
maxIterations: input.maxIterations ?? 10
|
|
13747
|
+
};
|
|
13748
|
+
}
|
|
13749
|
+
function normalizeFilter(input) {
|
|
13750
|
+
return {
|
|
13751
|
+
scenarioIds: input?.scenarioIds?.filter(Boolean),
|
|
13752
|
+
tags: input?.tags?.filter(Boolean),
|
|
13753
|
+
priority: input?.priority
|
|
13754
|
+
};
|
|
13755
|
+
}
|
|
13756
|
+
function normalizeExecution(input) {
|
|
13757
|
+
return input ? workflowExecutionFromValue(input) : DEFAULT_EXECUTION;
|
|
13758
|
+
}
|
|
13759
|
+
function createTestingWorkflow(input) {
|
|
13760
|
+
const db2 = getDatabase();
|
|
13761
|
+
const id = uuid();
|
|
13762
|
+
const timestamp = now();
|
|
13763
|
+
db2.query(`
|
|
13764
|
+
INSERT INTO testing_workflows
|
|
13765
|
+
(id, project_id, name, description, scenario_filter, persona_ids, goal, execution, settings, enabled, created_at, updated_at)
|
|
13766
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
13767
|
+
`).run(id, input.projectId ?? null, input.name, input.description ?? null, JSON.stringify(normalizeFilter(input.scenarioFilter)), JSON.stringify(input.personaIds ?? []), JSON.stringify(normalizeGoal(input.goal)), JSON.stringify(normalizeExecution(input.execution)), JSON.stringify(input.settings ?? {}), input.enabled === false ? 0 : 1, timestamp, timestamp);
|
|
13768
|
+
return getTestingWorkflow(id);
|
|
13769
|
+
}
|
|
13770
|
+
function getTestingWorkflow(id) {
|
|
13771
|
+
const db2 = getDatabase();
|
|
13772
|
+
let row = db2.query("SELECT * FROM testing_workflows WHERE id = ?").get(id);
|
|
13773
|
+
if (row)
|
|
13774
|
+
return workflowFromRow(row);
|
|
13775
|
+
const fullId = resolvePartialId("testing_workflows", id);
|
|
13776
|
+
if (fullId) {
|
|
13777
|
+
row = db2.query("SELECT * FROM testing_workflows WHERE id = ?").get(fullId);
|
|
13778
|
+
if (row)
|
|
13779
|
+
return workflowFromRow(row);
|
|
13780
|
+
}
|
|
13781
|
+
row = db2.query("SELECT * FROM testing_workflows WHERE name = ?").get(id);
|
|
13782
|
+
return row ? workflowFromRow(row) : null;
|
|
13783
|
+
}
|
|
13784
|
+
function listTestingWorkflows(filter) {
|
|
13785
|
+
const db2 = getDatabase();
|
|
13786
|
+
const conditions = [];
|
|
13787
|
+
const params = [];
|
|
13788
|
+
if (filter?.projectId) {
|
|
13789
|
+
conditions.push("project_id = ?");
|
|
13790
|
+
params.push(filter.projectId);
|
|
13791
|
+
}
|
|
13792
|
+
if (filter?.enabled !== undefined) {
|
|
13793
|
+
conditions.push("enabled = ?");
|
|
13794
|
+
params.push(filter.enabled ? 1 : 0);
|
|
13795
|
+
}
|
|
13796
|
+
const where = conditions.length > 0 ? ` WHERE ${conditions.join(" AND ")}` : "";
|
|
13797
|
+
const rows = db2.query(`SELECT * FROM testing_workflows${where} ORDER BY created_at DESC`).all(...params);
|
|
13798
|
+
return rows.map(workflowFromRow);
|
|
13799
|
+
}
|
|
13800
|
+
function updateTestingWorkflow(id, input) {
|
|
13801
|
+
const existing = getTestingWorkflow(id);
|
|
13802
|
+
if (!existing)
|
|
13803
|
+
throw new Error(`Testing workflow not found: ${id}`);
|
|
13804
|
+
const fields = [];
|
|
13805
|
+
const values = [];
|
|
13806
|
+
if (input.name !== undefined) {
|
|
13807
|
+
fields.push("name = ?");
|
|
13808
|
+
values.push(input.name);
|
|
13809
|
+
}
|
|
13810
|
+
if (input.description !== undefined) {
|
|
13811
|
+
fields.push("description = ?");
|
|
13812
|
+
values.push(input.description);
|
|
13813
|
+
}
|
|
13814
|
+
if (input.scenarioFilter !== undefined) {
|
|
13815
|
+
fields.push("scenario_filter = ?");
|
|
13816
|
+
values.push(JSON.stringify(normalizeFilter(input.scenarioFilter)));
|
|
13817
|
+
}
|
|
13818
|
+
if (input.personaIds !== undefined) {
|
|
13819
|
+
fields.push("persona_ids = ?");
|
|
13820
|
+
values.push(JSON.stringify(input.personaIds));
|
|
13821
|
+
}
|
|
13822
|
+
if (input.goal !== undefined) {
|
|
13823
|
+
fields.push("goal = ?");
|
|
13824
|
+
values.push(JSON.stringify(normalizeGoal(input.goal)));
|
|
13825
|
+
}
|
|
13826
|
+
if (input.execution !== undefined) {
|
|
13827
|
+
fields.push("execution = ?");
|
|
13828
|
+
values.push(JSON.stringify(normalizeExecution(input.execution)));
|
|
13829
|
+
}
|
|
13830
|
+
if (input.settings !== undefined) {
|
|
13831
|
+
fields.push("settings = ?");
|
|
13832
|
+
values.push(JSON.stringify(input.settings));
|
|
13833
|
+
}
|
|
13834
|
+
if (input.enabled !== undefined) {
|
|
13835
|
+
fields.push("enabled = ?");
|
|
13836
|
+
values.push(input.enabled ? 1 : 0);
|
|
13837
|
+
}
|
|
13838
|
+
if (fields.length === 0)
|
|
13839
|
+
return existing;
|
|
13840
|
+
fields.push("updated_at = ?");
|
|
13841
|
+
values.push(now(), existing.id);
|
|
13842
|
+
db2().query(`UPDATE testing_workflows SET ${fields.join(", ")} WHERE id = ?`).run(...values);
|
|
13843
|
+
return getTestingWorkflow(existing.id);
|
|
13844
|
+
}
|
|
13845
|
+
function deleteTestingWorkflow(id) {
|
|
13846
|
+
const existing = getTestingWorkflow(id);
|
|
13847
|
+
if (!existing)
|
|
13848
|
+
return false;
|
|
13849
|
+
return getDatabase().query("DELETE FROM testing_workflows WHERE id = ?").run(existing.id).changes > 0;
|
|
13850
|
+
}
|
|
13851
|
+
function db2() {
|
|
13852
|
+
return getDatabase();
|
|
13853
|
+
}
|
|
13611
13854
|
|
|
13612
13855
|
// src/index.ts
|
|
13613
13856
|
init_flows();
|
|
@@ -13878,11 +14121,11 @@ function resolveJudgeModel(config) {
|
|
|
13878
14121
|
apiKey = process.env["GOOGLE_API_KEY"];
|
|
13879
14122
|
else if (provider === "cerebras")
|
|
13880
14123
|
apiKey = process.env["CEREBRAS_API_KEY"];
|
|
14124
|
+
else if (provider === "zai")
|
|
14125
|
+
apiKey = process.env["ZAI_API_KEY"];
|
|
13881
14126
|
}
|
|
13882
14127
|
if (!apiKey) {
|
|
13883
|
-
|
|
13884
|
-
if (!apiKey)
|
|
13885
|
-
throw new AIClientError("No API key found for judge. Set ANTHROPIC_API_KEY, CEREBRAS_API_KEY, OPENAI_API_KEY, or GOOGLE_API_KEY.");
|
|
14128
|
+
throw new AIClientError(`No API key found for ${provider} judge provider.`);
|
|
13886
14129
|
}
|
|
13887
14130
|
return { model, provider, apiKey };
|
|
13888
14131
|
}
|
|
@@ -13897,11 +14140,11 @@ reason: 1-2 sentences max`;
|
|
|
13897
14140
|
async function callJudge(prompt, config) {
|
|
13898
14141
|
const { model, provider, apiKey } = resolveJudgeModel(config);
|
|
13899
14142
|
const threshold = 0.7;
|
|
13900
|
-
if (provider
|
|
13901
|
-
const
|
|
14143
|
+
if (provider !== "anthropic") {
|
|
14144
|
+
const compat = createOpenAICompatibleConfig(provider, apiKey);
|
|
13902
14145
|
const resp2 = await callOpenAICompatible({
|
|
13903
|
-
baseUrl,
|
|
13904
|
-
apiKey,
|
|
14146
|
+
baseUrl: compat.baseUrl,
|
|
14147
|
+
apiKey: compat.apiKey,
|
|
13905
14148
|
model,
|
|
13906
14149
|
system: LLM_SYSTEM,
|
|
13907
14150
|
messages: [{ role: "user", content: prompt }],
|
|
@@ -14905,20 +15148,20 @@ function loadBudgetConfig() {
|
|
|
14905
15148
|
};
|
|
14906
15149
|
}
|
|
14907
15150
|
function getCostSummary(options) {
|
|
14908
|
-
const
|
|
15151
|
+
const db3 = getDatabase();
|
|
14909
15152
|
const period = options?.period ?? "month";
|
|
14910
15153
|
const projectId = options?.projectId;
|
|
14911
15154
|
const dateFilter = getDateFilter(period);
|
|
14912
15155
|
const projectFilter = projectId ? "AND ru.project_id = ?" : "";
|
|
14913
15156
|
const projectParams = projectId ? [projectId] : [];
|
|
14914
|
-
const totalsRow =
|
|
15157
|
+
const totalsRow = db3.query(`SELECT
|
|
14915
15158
|
COALESCE(SUM(r.cost_cents), 0) as total_cost,
|
|
14916
15159
|
COALESCE(SUM(r.tokens_used), 0) as total_tokens,
|
|
14917
15160
|
COUNT(DISTINCT r.run_id) as run_count
|
|
14918
15161
|
FROM results r
|
|
14919
15162
|
JOIN runs ru ON r.run_id = ru.id
|
|
14920
15163
|
WHERE 1=1 ${dateFilter} ${projectFilter}`).get(...projectParams);
|
|
14921
|
-
const modelRows =
|
|
15164
|
+
const modelRows = db3.query(`SELECT
|
|
14922
15165
|
r.model,
|
|
14923
15166
|
COALESCE(SUM(r.cost_cents), 0) as cost_cents,
|
|
14924
15167
|
COALESCE(SUM(r.tokens_used), 0) as tokens,
|
|
@@ -14936,7 +15179,7 @@ function getCostSummary(options) {
|
|
|
14936
15179
|
runs: row.runs
|
|
14937
15180
|
};
|
|
14938
15181
|
}
|
|
14939
|
-
const scenarioRows =
|
|
15182
|
+
const scenarioRows = db3.query(`SELECT
|
|
14940
15183
|
r.scenario_id,
|
|
14941
15184
|
COALESCE(s.name, r.scenario_id) as name,
|
|
14942
15185
|
COALESCE(SUM(r.cost_cents), 0) as cost_cents,
|
|
@@ -15088,22 +15331,22 @@ function formatCostsJSON(summary) {
|
|
|
15088
15331
|
// src/db/step-results.ts
|
|
15089
15332
|
init_database();
|
|
15090
15333
|
function createStepResult(input) {
|
|
15091
|
-
const
|
|
15334
|
+
const db3 = getDatabase();
|
|
15092
15335
|
const id = uuid();
|
|
15093
15336
|
const timestamp = now();
|
|
15094
|
-
|
|
15337
|
+
db3.query(`
|
|
15095
15338
|
INSERT INTO step_results (id, result_id, step_number, action, status, tool_name, tool_input, thinking, created_at)
|
|
15096
15339
|
VALUES (?, ?, ?, ?, 'running', ?, ?, ?, ?)
|
|
15097
15340
|
`).run(id, input.resultId, input.stepNumber, input.action, input.toolName ?? null, input.toolInput ? JSON.stringify(input.toolInput) : null, input.thinking ?? null, timestamp);
|
|
15098
15341
|
return getStepResult(id);
|
|
15099
15342
|
}
|
|
15100
15343
|
function getStepResult(id) {
|
|
15101
|
-
const
|
|
15102
|
-
const row =
|
|
15344
|
+
const db3 = getDatabase();
|
|
15345
|
+
const row = db3.query("SELECT * FROM step_results WHERE id = ?").get(id);
|
|
15103
15346
|
return row ? stepResultFromRow(row) : null;
|
|
15104
15347
|
}
|
|
15105
15348
|
function updateStepResult(id, updates) {
|
|
15106
|
-
const
|
|
15349
|
+
const db3 = getDatabase();
|
|
15107
15350
|
const existing = getStepResult(id);
|
|
15108
15351
|
if (!existing)
|
|
15109
15352
|
return null;
|
|
@@ -15132,7 +15375,7 @@ function updateStepResult(id, updates) {
|
|
|
15132
15375
|
if (sets.length === 0)
|
|
15133
15376
|
return existing;
|
|
15134
15377
|
params.push(id);
|
|
15135
|
-
|
|
15378
|
+
db3.query(`UPDATE step_results SET ${sets.join(", ")} WHERE id = ?`).run(...params);
|
|
15136
15379
|
return getStepResult(id);
|
|
15137
15380
|
}
|
|
15138
15381
|
function stepResultFromRow(row) {
|
|
@@ -15157,18 +15400,18 @@ function stepResultFromRow(row) {
|
|
|
15157
15400
|
init_types();
|
|
15158
15401
|
init_database();
|
|
15159
15402
|
function getPersona(id) {
|
|
15160
|
-
const
|
|
15161
|
-
let row =
|
|
15403
|
+
const db3 = getDatabase();
|
|
15404
|
+
let row = db3.query("SELECT * FROM personas WHERE id = ?").get(id);
|
|
15162
15405
|
if (row)
|
|
15163
15406
|
return personaFromRow(row);
|
|
15164
|
-
row =
|
|
15407
|
+
row = db3.query("SELECT * FROM personas WHERE short_id = ?").get(id);
|
|
15165
15408
|
if (row)
|
|
15166
15409
|
return personaFromRow(row);
|
|
15167
15410
|
return null;
|
|
15168
15411
|
}
|
|
15169
15412
|
function savePersonaAuthCookies(id, cookies) {
|
|
15170
|
-
const
|
|
15171
|
-
|
|
15413
|
+
const db3 = getDatabase();
|
|
15414
|
+
db3.query("UPDATE personas SET auth_cookies = ?, updated_at = ? WHERE id = ?").run(JSON.stringify(cookies), now(), id);
|
|
15172
15415
|
}
|
|
15173
15416
|
|
|
15174
15417
|
// src/lib/runner.ts
|
|
@@ -15192,9 +15435,9 @@ function lookupFromVault(key) {
|
|
|
15192
15435
|
if (!existsSync9(vaultPath))
|
|
15193
15436
|
return null;
|
|
15194
15437
|
try {
|
|
15195
|
-
const
|
|
15196
|
-
const row =
|
|
15197
|
-
|
|
15438
|
+
const db3 = new Database2(vaultPath, { readonly: true });
|
|
15439
|
+
const row = db3.query("SELECT value FROM secrets WHERE key = ?").get(key);
|
|
15440
|
+
db3.close();
|
|
15198
15441
|
return row?.value ?? null;
|
|
15199
15442
|
} catch {
|
|
15200
15443
|
return null;
|
|
@@ -15458,21 +15701,21 @@ function fromRow(row) {
|
|
|
15458
15701
|
};
|
|
15459
15702
|
}
|
|
15460
15703
|
function createWebhook(input) {
|
|
15461
|
-
const
|
|
15704
|
+
const db3 = getDatabase();
|
|
15462
15705
|
const id = uuid();
|
|
15463
15706
|
const events = input.events ?? ["failed"];
|
|
15464
15707
|
const secret = input.secret ?? crypto.randomUUID().replace(/-/g, "");
|
|
15465
|
-
|
|
15708
|
+
db3.query(`
|
|
15466
15709
|
INSERT INTO webhooks (id, url, events, project_id, secret, active, created_at)
|
|
15467
15710
|
VALUES (?, ?, ?, ?, ?, 1, ?)
|
|
15468
15711
|
`).run(id, input.url, JSON.stringify(events), input.projectId ?? null, secret, now());
|
|
15469
15712
|
return getWebhook(id);
|
|
15470
15713
|
}
|
|
15471
15714
|
function getWebhook(id) {
|
|
15472
|
-
const
|
|
15473
|
-
const row =
|
|
15715
|
+
const db3 = getDatabase();
|
|
15716
|
+
const row = db3.query("SELECT * FROM webhooks WHERE id = ?").get(id);
|
|
15474
15717
|
if (!row) {
|
|
15475
|
-
const rows =
|
|
15718
|
+
const rows = db3.query("SELECT * FROM webhooks WHERE id LIKE ? || '%'").all(id);
|
|
15476
15719
|
if (rows.length === 1)
|
|
15477
15720
|
return fromRow(rows[0]);
|
|
15478
15721
|
return null;
|
|
@@ -15480,7 +15723,7 @@ function getWebhook(id) {
|
|
|
15480
15723
|
return fromRow(row);
|
|
15481
15724
|
}
|
|
15482
15725
|
function listWebhooks(projectId) {
|
|
15483
|
-
const
|
|
15726
|
+
const db3 = getDatabase();
|
|
15484
15727
|
let query = "SELECT * FROM webhooks WHERE active = 1";
|
|
15485
15728
|
const params = [];
|
|
15486
15729
|
if (projectId) {
|
|
@@ -15488,15 +15731,15 @@ function listWebhooks(projectId) {
|
|
|
15488
15731
|
params.push(projectId);
|
|
15489
15732
|
}
|
|
15490
15733
|
query += " ORDER BY created_at DESC";
|
|
15491
|
-
const rows =
|
|
15734
|
+
const rows = db3.query(query).all(...params);
|
|
15492
15735
|
return rows.map(fromRow);
|
|
15493
15736
|
}
|
|
15494
15737
|
function deleteWebhook(id) {
|
|
15495
|
-
const
|
|
15738
|
+
const db3 = getDatabase();
|
|
15496
15739
|
const webhook = getWebhook(id);
|
|
15497
15740
|
if (!webhook)
|
|
15498
15741
|
return false;
|
|
15499
|
-
|
|
15742
|
+
db3.query("DELETE FROM webhooks WHERE id = ?").run(webhook.id);
|
|
15500
15743
|
return true;
|
|
15501
15744
|
}
|
|
15502
15745
|
function signPayload(body, secret) {
|
|
@@ -15664,12 +15907,12 @@ function connectToTodos(options = {}) {
|
|
|
15664
15907
|
if (!existsSync10(dbPath)) {
|
|
15665
15908
|
throw new TodosConnectionError(`Todos database not found at ${dbPath}. Install @hasna/todos or set TODOS_DB_PATH.`);
|
|
15666
15909
|
}
|
|
15667
|
-
const
|
|
15668
|
-
|
|
15669
|
-
return
|
|
15910
|
+
const db3 = new Database3(dbPath, { readonly: options.readonly ?? true });
|
|
15911
|
+
db3.exec("PRAGMA foreign_keys = ON");
|
|
15912
|
+
return db3;
|
|
15670
15913
|
}
|
|
15671
15914
|
function pullTasks(options = {}) {
|
|
15672
|
-
const
|
|
15915
|
+
const db3 = connectToTodos({ readonly: true });
|
|
15673
15916
|
try {
|
|
15674
15917
|
let query = "SELECT id, short_id, title, description, status, priority, tags, project_id FROM tasks WHERE 1=1";
|
|
15675
15918
|
const params = [];
|
|
@@ -15684,14 +15927,14 @@ function pullTasks(options = {}) {
|
|
|
15684
15927
|
params.push(options.priority);
|
|
15685
15928
|
}
|
|
15686
15929
|
if (options.projectName) {
|
|
15687
|
-
const project =
|
|
15930
|
+
const project = db3.query("SELECT id FROM projects WHERE name = ?").get(options.projectName);
|
|
15688
15931
|
if (project) {
|
|
15689
15932
|
query += " AND project_id = ?";
|
|
15690
15933
|
params.push(project.id);
|
|
15691
15934
|
}
|
|
15692
15935
|
}
|
|
15693
15936
|
query += " ORDER BY CASE priority WHEN 'critical' THEN 0 WHEN 'high' THEN 1 WHEN 'medium' THEN 2 WHEN 'low' THEN 3 END";
|
|
15694
|
-
const tasks =
|
|
15937
|
+
const tasks = db3.query(query).all(...params);
|
|
15695
15938
|
if (options.tags && options.tags.length > 0) {
|
|
15696
15939
|
return tasks.filter((task) => {
|
|
15697
15940
|
const taskTags = JSON.parse(task.tags || "[]");
|
|
@@ -15700,7 +15943,7 @@ function pullTasks(options = {}) {
|
|
|
15700
15943
|
}
|
|
15701
15944
|
return tasks;
|
|
15702
15945
|
} finally {
|
|
15703
|
-
|
|
15946
|
+
db3.close();
|
|
15704
15947
|
}
|
|
15705
15948
|
}
|
|
15706
15949
|
function taskToScenarioInput(task, projectId) {
|
|
@@ -15752,15 +15995,15 @@ function markTodoDone(taskId) {
|
|
|
15752
15995
|
const dbPath = resolveTodosDbPath();
|
|
15753
15996
|
if (!existsSync10(dbPath))
|
|
15754
15997
|
return false;
|
|
15755
|
-
const
|
|
15998
|
+
const db3 = new Database3(dbPath);
|
|
15756
15999
|
try {
|
|
15757
|
-
const task =
|
|
16000
|
+
const task = db3.query("SELECT id, version FROM tasks WHERE id LIKE ? || '%'").get(taskId);
|
|
15758
16001
|
if (!task)
|
|
15759
16002
|
return false;
|
|
15760
|
-
|
|
16003
|
+
db3.query("UPDATE tasks SET status = 'completed', completed_at = datetime('now'), version = version + 1, updated_at = datetime('now') WHERE id = ? AND version = ?").run(task.id, task.version);
|
|
15761
16004
|
return true;
|
|
15762
16005
|
} finally {
|
|
15763
|
-
|
|
16006
|
+
db3.close();
|
|
15764
16007
|
}
|
|
15765
16008
|
}
|
|
15766
16009
|
|
|
@@ -15771,9 +16014,9 @@ async function createFailureTasks(run, failedResults, scenarios) {
|
|
|
15771
16014
|
const projectId = process.env["TESTERS_TODOS_PROJECT_ID"];
|
|
15772
16015
|
if (!projectId)
|
|
15773
16016
|
return { created: 0, skipped: 0 };
|
|
15774
|
-
let
|
|
16017
|
+
let db3 = null;
|
|
15775
16018
|
try {
|
|
15776
|
-
|
|
16019
|
+
db3 = connectToTodos({ readonly: false });
|
|
15777
16020
|
} catch {
|
|
15778
16021
|
return { created: 0, skipped: 0 };
|
|
15779
16022
|
}
|
|
@@ -15784,7 +16027,7 @@ async function createFailureTasks(run, failedResults, scenarios) {
|
|
|
15784
16027
|
for (const result of failedResults) {
|
|
15785
16028
|
const scenario = scenarioMap.get(result.scenarioId);
|
|
15786
16029
|
const title = `BUG: [testers] ${scenario?.name ?? result.scenarioId} failed`;
|
|
15787
|
-
const existing =
|
|
16030
|
+
const existing = db3.query("SELECT id FROM tasks WHERE title = ? AND status NOT IN ('completed', 'cancelled') LIMIT 1").get(title);
|
|
15788
16031
|
if (existing) {
|
|
15789
16032
|
skipped++;
|
|
15790
16033
|
continue;
|
|
@@ -15805,7 +16048,7 @@ async function createFailureTasks(run, failedResults, scenarios) {
|
|
|
15805
16048
|
].filter(Boolean).join(`
|
|
15806
16049
|
`);
|
|
15807
16050
|
try {
|
|
15808
|
-
|
|
16051
|
+
db3.query(`
|
|
15809
16052
|
INSERT INTO tasks (id, short_id, title, description, status, priority, tags, project_id, version, created_at, updated_at)
|
|
15810
16053
|
VALUES (?, ?, ?, ?, 'pending', 'high', ?, ?, 1, ?, ?)
|
|
15811
16054
|
`).run(id, `BUG-${id.slice(0, 6)}`, title, description, JSON.stringify(["bug", "testers", "auto-created"]), projectId, now2, now2);
|
|
@@ -15815,7 +16058,7 @@ async function createFailureTasks(run, failedResults, scenarios) {
|
|
|
15815
16058
|
}
|
|
15816
16059
|
}
|
|
15817
16060
|
} finally {
|
|
15818
|
-
|
|
16061
|
+
db3.close();
|
|
15819
16062
|
}
|
|
15820
16063
|
return { created, skipped };
|
|
15821
16064
|
}
|
|
@@ -15894,6 +16137,291 @@ async function notifyRunToConversations(run, results, options) {
|
|
|
15894
16137
|
} catch {}
|
|
15895
16138
|
}
|
|
15896
16139
|
|
|
16140
|
+
// src/lib/a11y-audit.ts
|
|
16141
|
+
async function runA11yAudit(page, options = {}) {
|
|
16142
|
+
const { level = "AA", rules, exclude = [] } = options;
|
|
16143
|
+
await page.addScriptTag({ url: "https://cdnjs.cloudflare.com/ajax/libs/axe-core/4.9.1/axe.min.js" });
|
|
16144
|
+
const config = {
|
|
16145
|
+
runOnly: {
|
|
16146
|
+
type: level === "AAA" ? "standard" : "tag",
|
|
16147
|
+
values: level === "AAA" ? undefined : [level, "best-practice"]
|
|
16148
|
+
}
|
|
16149
|
+
};
|
|
16150
|
+
if (rules && rules.length > 0) {
|
|
16151
|
+
config.rules = Object.fromEntries(rules.map((r) => [r, { enabled: true }]));
|
|
16152
|
+
}
|
|
16153
|
+
if (exclude.length > 0) {
|
|
16154
|
+
config.exclude = exclude;
|
|
16155
|
+
}
|
|
16156
|
+
const result = await page.evaluate(async (auditConfig) => {
|
|
16157
|
+
const axeResult = await window.axe.run(auditConfig);
|
|
16158
|
+
return axeResult;
|
|
16159
|
+
}, config);
|
|
16160
|
+
const violations = (result.violations ?? []).map((v) => ({
|
|
16161
|
+
id: v.id,
|
|
16162
|
+
impact: v.impact,
|
|
16163
|
+
description: v.description,
|
|
16164
|
+
help: v.help,
|
|
16165
|
+
helpUrl: v.helpUrl,
|
|
16166
|
+
nodes: (v.nodes ?? []).map((n) => ({
|
|
16167
|
+
html: n.html,
|
|
16168
|
+
target: n.target,
|
|
16169
|
+
failureSummary: n.failureSummary
|
|
16170
|
+
}))
|
|
16171
|
+
}));
|
|
16172
|
+
const passes = (result.passes ?? []).map((p) => ({
|
|
16173
|
+
id: p.id,
|
|
16174
|
+
description: p.description
|
|
16175
|
+
}));
|
|
16176
|
+
const incomplete = (result.incomplete ?? []).map((i) => ({
|
|
16177
|
+
id: i.id,
|
|
16178
|
+
description: i.description,
|
|
16179
|
+
impact: i.impact
|
|
16180
|
+
}));
|
|
16181
|
+
const criticalCount = violations.filter((v) => v.impact === "critical").length;
|
|
16182
|
+
const seriousCount = violations.filter((v) => v.impact === "serious").length;
|
|
16183
|
+
const moderateCount = violations.filter((v) => v.impact === "moderate").length;
|
|
16184
|
+
const minorCount = violations.filter((v) => v.impact === "minor").length;
|
|
16185
|
+
return {
|
|
16186
|
+
violations,
|
|
16187
|
+
passes,
|
|
16188
|
+
incomplete,
|
|
16189
|
+
url: page.url(),
|
|
16190
|
+
timestamp: new Date().toISOString(),
|
|
16191
|
+
totalViolations: violations.length,
|
|
16192
|
+
criticalCount,
|
|
16193
|
+
seriousCount,
|
|
16194
|
+
moderateCount,
|
|
16195
|
+
minorCount
|
|
16196
|
+
};
|
|
16197
|
+
}
|
|
16198
|
+
|
|
16199
|
+
// src/lib/assertions.ts
|
|
16200
|
+
async function evaluateAssertions(page, assertions, context = {}) {
|
|
16201
|
+
const results = [];
|
|
16202
|
+
for (const assertion of assertions) {
|
|
16203
|
+
try {
|
|
16204
|
+
const result = await evaluateOne(page, assertion, context);
|
|
16205
|
+
results.push(result);
|
|
16206
|
+
} catch (err) {
|
|
16207
|
+
results.push({
|
|
16208
|
+
assertion,
|
|
16209
|
+
passed: false,
|
|
16210
|
+
actual: "",
|
|
16211
|
+
error: err instanceof Error ? err.message : String(err)
|
|
16212
|
+
});
|
|
16213
|
+
}
|
|
16214
|
+
}
|
|
16215
|
+
return results;
|
|
16216
|
+
}
|
|
16217
|
+
async function evaluateOne(page, assertion, context) {
|
|
16218
|
+
switch (assertion.type) {
|
|
16219
|
+
case "visible": {
|
|
16220
|
+
const visible = await page.locator(assertion.selector).isVisible();
|
|
16221
|
+
return {
|
|
16222
|
+
assertion,
|
|
16223
|
+
passed: visible,
|
|
16224
|
+
actual: String(visible)
|
|
16225
|
+
};
|
|
16226
|
+
}
|
|
16227
|
+
case "not_visible": {
|
|
16228
|
+
const visible = await page.locator(assertion.selector).isVisible();
|
|
16229
|
+
return {
|
|
16230
|
+
assertion,
|
|
16231
|
+
passed: !visible,
|
|
16232
|
+
actual: String(visible)
|
|
16233
|
+
};
|
|
16234
|
+
}
|
|
16235
|
+
case "text_contains": {
|
|
16236
|
+
const text = await page.locator(assertion.selector).textContent() ?? "";
|
|
16237
|
+
const expected = String(assertion.expected ?? "");
|
|
16238
|
+
return {
|
|
16239
|
+
assertion,
|
|
16240
|
+
passed: text.includes(expected),
|
|
16241
|
+
actual: text
|
|
16242
|
+
};
|
|
16243
|
+
}
|
|
16244
|
+
case "text_equals": {
|
|
16245
|
+
const text = await page.locator(assertion.selector).textContent() ?? "";
|
|
16246
|
+
const expected = String(assertion.expected ?? "");
|
|
16247
|
+
return {
|
|
16248
|
+
assertion,
|
|
16249
|
+
passed: text.trim() === expected.trim(),
|
|
16250
|
+
actual: text
|
|
16251
|
+
};
|
|
16252
|
+
}
|
|
16253
|
+
case "element_count": {
|
|
16254
|
+
const count = await page.locator(assertion.selector).count();
|
|
16255
|
+
const expected = Number(assertion.expected ?? 0);
|
|
16256
|
+
return {
|
|
16257
|
+
assertion,
|
|
16258
|
+
passed: count === expected,
|
|
16259
|
+
actual: String(count)
|
|
16260
|
+
};
|
|
16261
|
+
}
|
|
16262
|
+
case "no_console_errors": {
|
|
16263
|
+
if (context.consoleErrors !== undefined) {
|
|
16264
|
+
const errors = context.consoleErrors.filter(Boolean);
|
|
16265
|
+
return {
|
|
16266
|
+
assertion,
|
|
16267
|
+
passed: errors.length === 0,
|
|
16268
|
+
actual: errors.length === 0 ? "No console errors captured" : errors.slice(0, 3).join(" | ")
|
|
16269
|
+
};
|
|
16270
|
+
}
|
|
16271
|
+
const errorElements = await page.locator('[role="alert"], .error, .error-message, [data-testid="error"]').count();
|
|
16272
|
+
return {
|
|
16273
|
+
assertion,
|
|
16274
|
+
passed: errorElements === 0,
|
|
16275
|
+
actual: `${errorElements} error element(s) found`
|
|
16276
|
+
};
|
|
16277
|
+
}
|
|
16278
|
+
case "no_a11y_violations": {
|
|
16279
|
+
try {
|
|
16280
|
+
const auditResult = await runA11yAudit(page);
|
|
16281
|
+
const hasIssues = auditResult.violations.length > 0;
|
|
16282
|
+
return {
|
|
16283
|
+
assertion,
|
|
16284
|
+
passed: !hasIssues,
|
|
16285
|
+
actual: hasIssues ? `${auditResult.totalViolations} violation(s): ${auditResult.violations.map((v) => v.id).join(", ")}` : "No accessibility violations found"
|
|
16286
|
+
};
|
|
16287
|
+
} catch (err) {
|
|
16288
|
+
return {
|
|
16289
|
+
assertion,
|
|
16290
|
+
passed: false,
|
|
16291
|
+
actual: "",
|
|
16292
|
+
error: err instanceof Error ? err.message : String(err)
|
|
16293
|
+
};
|
|
16294
|
+
}
|
|
16295
|
+
}
|
|
16296
|
+
case "url_contains": {
|
|
16297
|
+
const url = page.url();
|
|
16298
|
+
const expected = String(assertion.expected ?? "");
|
|
16299
|
+
return {
|
|
16300
|
+
assertion,
|
|
16301
|
+
passed: url.includes(expected),
|
|
16302
|
+
actual: url
|
|
16303
|
+
};
|
|
16304
|
+
}
|
|
16305
|
+
case "title_contains": {
|
|
16306
|
+
const title = await page.title();
|
|
16307
|
+
const expected = String(assertion.expected ?? "");
|
|
16308
|
+
return {
|
|
16309
|
+
assertion,
|
|
16310
|
+
passed: title.includes(expected),
|
|
16311
|
+
actual: title
|
|
16312
|
+
};
|
|
16313
|
+
}
|
|
16314
|
+
case "cookie_exists": {
|
|
16315
|
+
const cookieName = assertion.expected;
|
|
16316
|
+
const cookies = await page.context().cookies();
|
|
16317
|
+
const found = cookies.some((c) => c.name === cookieName);
|
|
16318
|
+
return {
|
|
16319
|
+
assertion,
|
|
16320
|
+
passed: found,
|
|
16321
|
+
actual: found ? `Cookie "${cookieName}" exists` : `Cookie "${cookieName}" not found`
|
|
16322
|
+
};
|
|
16323
|
+
}
|
|
16324
|
+
case "cookie_not_exists": {
|
|
16325
|
+
const cookieName = assertion.expected;
|
|
16326
|
+
const cookies = await page.context().cookies();
|
|
16327
|
+
const found = cookies.some((c) => c.name === cookieName);
|
|
16328
|
+
return {
|
|
16329
|
+
assertion,
|
|
16330
|
+
passed: !found,
|
|
16331
|
+
actual: found ? `Cookie "${cookieName}" found (unexpected)` : `Cookie "${cookieName}" does not exist`
|
|
16332
|
+
};
|
|
16333
|
+
}
|
|
16334
|
+
case "cookie_value": {
|
|
16335
|
+
const [cookieName, expectedValue] = assertion.expected.split("=", 2);
|
|
16336
|
+
const cookies = await page.context().cookies();
|
|
16337
|
+
const cookie = cookies.find((c) => c.name === cookieName);
|
|
16338
|
+
const actualValue = cookie?.value ?? "";
|
|
16339
|
+
return {
|
|
16340
|
+
assertion,
|
|
16341
|
+
passed: actualValue === expectedValue,
|
|
16342
|
+
actual: cookie ? `${cookieName}=${actualValue}` : `Cookie "${cookieName}" not found`
|
|
16343
|
+
};
|
|
16344
|
+
}
|
|
16345
|
+
case "local_storage_exists": {
|
|
16346
|
+
const key = assertion.expected;
|
|
16347
|
+
const value = await page.evaluate((k) => localStorage.getItem(k), key);
|
|
16348
|
+
return {
|
|
16349
|
+
assertion,
|
|
16350
|
+
passed: value !== null,
|
|
16351
|
+
actual: value !== null ? `Key "${key}" exists with value "${value}"` : `Key "${key}" not found in localStorage`
|
|
16352
|
+
};
|
|
16353
|
+
}
|
|
16354
|
+
case "local_storage_not_exists": {
|
|
16355
|
+
const key = assertion.expected;
|
|
16356
|
+
const value = await page.evaluate((k) => localStorage.getItem(k), key);
|
|
16357
|
+
return {
|
|
16358
|
+
assertion,
|
|
16359
|
+
passed: value === null,
|
|
16360
|
+
actual: value !== null ? `Key "${key}" exists (unexpected)` : `Key "${key}" does not exist in localStorage`
|
|
16361
|
+
};
|
|
16362
|
+
}
|
|
16363
|
+
case "local_storage_value": {
|
|
16364
|
+
const [lsKey, expectedValue] = assertion.expected.split("=", 2);
|
|
16365
|
+
const value = await page.evaluate((k) => localStorage.getItem(k), lsKey ?? "");
|
|
16366
|
+
return {
|
|
16367
|
+
assertion,
|
|
16368
|
+
passed: value === expectedValue,
|
|
16369
|
+
actual: value !== null ? `${lsKey}=${value}` : `Key "${lsKey}" not found in localStorage`
|
|
16370
|
+
};
|
|
16371
|
+
}
|
|
16372
|
+
case "session_storage_value": {
|
|
16373
|
+
const [ssKey, expectedValue] = assertion.expected.split("=", 2);
|
|
16374
|
+
const value = await page.evaluate((k) => sessionStorage.getItem(k), ssKey ?? "");
|
|
16375
|
+
return {
|
|
16376
|
+
assertion,
|
|
16377
|
+
passed: value === expectedValue,
|
|
16378
|
+
actual: value !== null ? `${ssKey}=${value}` : `Key "${ssKey}" not found in sessionStorage`
|
|
16379
|
+
};
|
|
16380
|
+
}
|
|
16381
|
+
case "session_storage_not_exists": {
|
|
16382
|
+
const key = assertion.expected;
|
|
16383
|
+
const value = await page.evaluate((k) => sessionStorage.getItem(k), key);
|
|
16384
|
+
return {
|
|
16385
|
+
assertion,
|
|
16386
|
+
passed: value === null,
|
|
16387
|
+
actual: value !== null ? `Key "${key}" exists (unexpected)` : `Key "${key}" does not exist in sessionStorage`
|
|
16388
|
+
};
|
|
16389
|
+
}
|
|
16390
|
+
default: {
|
|
16391
|
+
return {
|
|
16392
|
+
assertion,
|
|
16393
|
+
passed: false,
|
|
16394
|
+
actual: "",
|
|
16395
|
+
error: `Unknown assertion type: ${assertion.type}`
|
|
16396
|
+
};
|
|
16397
|
+
}
|
|
16398
|
+
}
|
|
16399
|
+
}
|
|
16400
|
+
function allAssertionsPassed(results) {
|
|
16401
|
+
return results.every((r) => r.passed);
|
|
16402
|
+
}
|
|
16403
|
+
function formatAssertionResults(results) {
|
|
16404
|
+
if (results.length === 0)
|
|
16405
|
+
return "No assertions.";
|
|
16406
|
+
const lines = [];
|
|
16407
|
+
for (const r of results) {
|
|
16408
|
+
const icon = r.passed ? "PASS" : "FAIL";
|
|
16409
|
+
const desc = r.assertion.description || `${r.assertion.type}${r.assertion.selector ? ` ${r.assertion.selector}` : ""}`;
|
|
16410
|
+
let line = ` [${icon}] ${desc}`;
|
|
16411
|
+
if (!r.passed) {
|
|
16412
|
+
line += ` (actual: ${r.actual})`;
|
|
16413
|
+
if (r.error)
|
|
16414
|
+
line += ` \u2014 ${r.error}`;
|
|
16415
|
+
}
|
|
16416
|
+
lines.push(line);
|
|
16417
|
+
}
|
|
16418
|
+
const passed = results.filter((r) => r.passed).length;
|
|
16419
|
+
lines.push(`
|
|
16420
|
+
${passed}/${results.length} assertions passed.`);
|
|
16421
|
+
return lines.join(`
|
|
16422
|
+
`);
|
|
16423
|
+
}
|
|
16424
|
+
|
|
15897
16425
|
// src/lib/runner.ts
|
|
15898
16426
|
var eventHandler = null;
|
|
15899
16427
|
function onRunEvent(handler) {
|
|
@@ -15903,6 +16431,57 @@ function emit(event) {
|
|
|
15903
16431
|
if (eventHandler)
|
|
15904
16432
|
eventHandler(event);
|
|
15905
16433
|
}
|
|
16434
|
+
function resolveAgentApiKeyForModel(model, explicitApiKey, configuredAnthropicApiKey) {
|
|
16435
|
+
return resolveProviderApiKeyForModel(model, explicitApiKey, configuredAnthropicApiKey);
|
|
16436
|
+
}
|
|
16437
|
+
function assertionDescription(result) {
|
|
16438
|
+
return result.assertion.description || `${result.assertion.type}${result.assertion.selector ? ` ${result.assertion.selector}` : ""}`;
|
|
16439
|
+
}
|
|
16440
|
+
function summarizeAssertionResult(result) {
|
|
16441
|
+
const description = assertionDescription(result);
|
|
16442
|
+
if (result.passed)
|
|
16443
|
+
return description;
|
|
16444
|
+
const suffix = result.error ? `; ${result.error}` : "";
|
|
16445
|
+
return `${description} (actual: ${result.actual}${suffix})`;
|
|
16446
|
+
}
|
|
16447
|
+
async function applyStructuredAssertionsToResult(input) {
|
|
16448
|
+
const assertions = input.scenario.assertions ?? [];
|
|
16449
|
+
if (assertions.length === 0) {
|
|
16450
|
+
return {
|
|
16451
|
+
status: input.status,
|
|
16452
|
+
reasoning: input.reasoning,
|
|
16453
|
+
assertionsPassed: [],
|
|
16454
|
+
assertionsFailed: [],
|
|
16455
|
+
assertionResults: []
|
|
16456
|
+
};
|
|
16457
|
+
}
|
|
16458
|
+
const results = await evaluateAssertions(input.page, assertions, {
|
|
16459
|
+
consoleErrors: input.consoleErrors
|
|
16460
|
+
});
|
|
16461
|
+
const assertionsPassed = results.filter((r) => r.passed).map(summarizeAssertionResult);
|
|
16462
|
+
const assertionsFailed = results.filter((r) => !r.passed).map(summarizeAssertionResult);
|
|
16463
|
+
const assertionResults = results.map((result) => ({
|
|
16464
|
+
type: result.assertion.type,
|
|
16465
|
+
description: assertionDescription(result),
|
|
16466
|
+
passed: result.passed,
|
|
16467
|
+
actual: result.actual,
|
|
16468
|
+
...result.error ? { error: result.error } : {}
|
|
16469
|
+
}));
|
|
16470
|
+
const assertionsOk = allAssertionsPassed(results);
|
|
16471
|
+
const status = assertionsOk || input.status !== "passed" ? input.status : "failed";
|
|
16472
|
+
const assertionHeading = assertionsOk ? "Structured assertions passed:" : "Structured assertions failed:";
|
|
16473
|
+
const reasoningParts = [input.reasoning, `${assertionHeading}
|
|
16474
|
+
${formatAssertionResults(results)}`].map((part) => part.trim()).filter(Boolean);
|
|
16475
|
+
return {
|
|
16476
|
+
status,
|
|
16477
|
+
reasoning: reasoningParts.join(`
|
|
16478
|
+
|
|
16479
|
+
`),
|
|
16480
|
+
assertionsPassed,
|
|
16481
|
+
assertionsFailed,
|
|
16482
|
+
assertionResults
|
|
16483
|
+
};
|
|
16484
|
+
}
|
|
15906
16485
|
function withTimeout(promise, ms, label) {
|
|
15907
16486
|
return new Promise((resolve, reject) => {
|
|
15908
16487
|
const warningAt = Math.floor(ms * 0.8);
|
|
@@ -15963,7 +16542,7 @@ async function runSingleScenario(scenario, runId, options) {
|
|
|
15963
16542
|
});
|
|
15964
16543
|
}
|
|
15965
16544
|
}
|
|
15966
|
-
const client = createClientForModel(model, effectiveOptions.apiKey
|
|
16545
|
+
const client = createClientForModel(model, resolveAgentApiKeyForModel(model, effectiveOptions.apiKey, config.anthropicApiKey));
|
|
15967
16546
|
const screenshotter = new Screenshotter({
|
|
15968
16547
|
baseDir: effectiveOptions.screenshotDir ?? config.screenshots.dir
|
|
15969
16548
|
});
|
|
@@ -16073,6 +16652,7 @@ async function runSingleScenario(scenario, runId, options) {
|
|
|
16073
16652
|
model,
|
|
16074
16653
|
runId,
|
|
16075
16654
|
sessionId: result.id,
|
|
16655
|
+
baseUrl: options.url,
|
|
16076
16656
|
maxTurns: effectiveOptions.minimal ? 10 : 30,
|
|
16077
16657
|
a11y: effectiveOptions.a11y,
|
|
16078
16658
|
persona: persona ? {
|
|
@@ -16155,27 +16735,46 @@ async function runSingleScenario(scenario, runId, options) {
|
|
|
16155
16735
|
closeSession(result.id);
|
|
16156
16736
|
const lightpandaNote = options.engine === "lightpanda" ? " (Running with Lightpanda \u2014 no screenshots)" : options.engine === "bun" ? " (Running with Bun.WebView \u2014 native, ~11x faster)" : "";
|
|
16157
16737
|
const networkMeta = networkErrors.length > 0 ? { networkErrors: networkErrors.slice(0, 20) } : {};
|
|
16158
|
-
|
|
16738
|
+
const baseReasoning = agentResult.reasoning ? agentResult.reasoning + lightpandaNote : lightpandaNote || "";
|
|
16739
|
+
const assertionOutcome = await applyStructuredAssertionsToResult({
|
|
16740
|
+
page,
|
|
16741
|
+
scenario,
|
|
16742
|
+
consoleErrors,
|
|
16159
16743
|
status: agentResult.status,
|
|
16160
|
-
reasoning:
|
|
16744
|
+
reasoning: baseReasoning
|
|
16745
|
+
});
|
|
16746
|
+
const structuredAssertionMeta = assertionOutcome.assertionResults.length > 0 ? {
|
|
16747
|
+
structuredAssertions: {
|
|
16748
|
+
passed: assertionOutcome.assertionsPassed,
|
|
16749
|
+
failed: assertionOutcome.assertionsFailed,
|
|
16750
|
+
results: assertionOutcome.assertionResults
|
|
16751
|
+
}
|
|
16752
|
+
} : {};
|
|
16753
|
+
let updatedResult = updateResult(result.id, {
|
|
16754
|
+
status: assertionOutcome.status,
|
|
16755
|
+
reasoning: assertionOutcome.reasoning || undefined,
|
|
16161
16756
|
stepsCompleted: agentResult.stepsCompleted,
|
|
16162
16757
|
durationMs: Date.now() - new Date(result.createdAt).getTime(),
|
|
16163
16758
|
tokensUsed: agentResult.tokensUsed,
|
|
16164
16759
|
costCents: estimateCost(model, agentResult.tokensUsed),
|
|
16165
|
-
metadata: {
|
|
16760
|
+
metadata: {
|
|
16761
|
+
consoleLogs,
|
|
16762
|
+
...networkErrors.length > 0 ? networkMeta : {},
|
|
16763
|
+
...structuredAssertionMeta
|
|
16764
|
+
}
|
|
16166
16765
|
});
|
|
16167
|
-
if (
|
|
16168
|
-
const failureAnalysis = analyzeFailure(null,
|
|
16766
|
+
if (assertionOutcome.status === "failed" || assertionOutcome.status === "error") {
|
|
16767
|
+
const failureAnalysis = analyzeFailure(null, assertionOutcome.reasoning ?? null);
|
|
16169
16768
|
if (failureAnalysis) {
|
|
16170
16769
|
updatedResult = updateResult(result.id, { failureAnalysis });
|
|
16171
16770
|
}
|
|
16172
16771
|
}
|
|
16173
|
-
if (
|
|
16772
|
+
if (assertionOutcome.status === "passed") {
|
|
16174
16773
|
try {
|
|
16175
16774
|
updateScenarioPassedCache(scenario.id, options.url);
|
|
16176
16775
|
} catch {}
|
|
16177
16776
|
}
|
|
16178
|
-
const eventType =
|
|
16777
|
+
const eventType = assertionOutcome.status === "passed" ? "scenario:pass" : "scenario:fail";
|
|
16179
16778
|
emit({ type: eventType, scenarioId: scenario.id, scenarioName: scenario.name, resultId: result.id, runId });
|
|
16180
16779
|
return updatedResult;
|
|
16181
16780
|
} catch (error) {
|
|
@@ -16200,7 +16799,8 @@ async function runSingleScenario(scenario, runId, options) {
|
|
|
16200
16799
|
} finally {
|
|
16201
16800
|
if (harPath) {
|
|
16202
16801
|
try {
|
|
16203
|
-
|
|
16802
|
+
const existing = getResult(result.id);
|
|
16803
|
+
updateResult(result.id, { metadata: { ...existing?.metadata ?? {}, harPath } });
|
|
16204
16804
|
} catch {}
|
|
16205
16805
|
}
|
|
16206
16806
|
if (browser) {
|
|
@@ -16372,22 +16972,31 @@ async function runBatch(scenarios, options) {
|
|
|
16372
16972
|
}
|
|
16373
16973
|
return { run: finalRun, results };
|
|
16374
16974
|
}
|
|
16375
|
-
|
|
16376
|
-
|
|
16975
|
+
function findScenarioInList(scenarios, id) {
|
|
16976
|
+
return scenarios.find((scenario) => scenario.id === id || scenario.shortId === id || scenario.id.startsWith(id)) ?? null;
|
|
16977
|
+
}
|
|
16978
|
+
function resolveScenariosForRun(options) {
|
|
16377
16979
|
if (options.scenarioIds && options.scenarioIds.length > 0) {
|
|
16378
|
-
const
|
|
16379
|
-
|
|
16380
|
-
|
|
16381
|
-
|
|
16382
|
-
|
|
16980
|
+
const scoped = listScenarios({ projectId: options.projectId });
|
|
16981
|
+
const resolved = [];
|
|
16982
|
+
const seen = new Set;
|
|
16983
|
+
for (const id of options.scenarioIds) {
|
|
16984
|
+
const scenario = findScenarioInList(scoped, id) ?? getScenario(id);
|
|
16985
|
+
if (scenario && !seen.has(scenario.id)) {
|
|
16986
|
+
resolved.push(scenario);
|
|
16987
|
+
seen.add(scenario.id);
|
|
16988
|
+
}
|
|
16383
16989
|
}
|
|
16384
|
-
|
|
16385
|
-
scenarios = listScenarios({
|
|
16386
|
-
projectId: options.projectId,
|
|
16387
|
-
tags: options.tags,
|
|
16388
|
-
priority: options.priority
|
|
16389
|
-
});
|
|
16990
|
+
return resolved;
|
|
16390
16991
|
}
|
|
16992
|
+
return listScenarios({
|
|
16993
|
+
projectId: options.projectId,
|
|
16994
|
+
tags: options.tags,
|
|
16995
|
+
priority: options.priority
|
|
16996
|
+
});
|
|
16997
|
+
}
|
|
16998
|
+
async function runByFilter(options) {
|
|
16999
|
+
const scenarios = resolveScenariosForRun(options);
|
|
16391
17000
|
if (scenarios.length === 0) {
|
|
16392
17001
|
const config = loadConfig();
|
|
16393
17002
|
const model = resolveModel2(options.model ?? config.defaultModel);
|
|
@@ -16400,17 +17009,7 @@ async function runByFilter(options) {
|
|
|
16400
17009
|
function startRunAsync(options) {
|
|
16401
17010
|
const config = loadConfig();
|
|
16402
17011
|
const model = resolveModel2(options.model ?? config.defaultModel);
|
|
16403
|
-
|
|
16404
|
-
if (options.scenarioIds && options.scenarioIds.length > 0) {
|
|
16405
|
-
const all = listScenarios({ projectId: options.projectId });
|
|
16406
|
-
scenarios = all.filter((s) => options.scenarioIds.includes(s.id) || options.scenarioIds.includes(s.shortId));
|
|
16407
|
-
} else {
|
|
16408
|
-
scenarios = listScenarios({
|
|
16409
|
-
projectId: options.projectId,
|
|
16410
|
-
tags: options.tags,
|
|
16411
|
-
priority: options.priority
|
|
16412
|
-
});
|
|
16413
|
-
}
|
|
17012
|
+
const scenarios = resolveScenariosForRun(options);
|
|
16414
17013
|
if (!options.skipBudgetCheck) {
|
|
16415
17014
|
const cap = options.maxCostCents ?? config.defaultMaxCostCents;
|
|
16416
17015
|
if (cap !== undefined && cap > 0 && scenarios.length > 0) {
|
|
@@ -16495,6 +17094,170 @@ function estimateCost(model, tokens) {
|
|
|
16495
17094
|
const costPer1M = costs[model] ?? 0.5;
|
|
16496
17095
|
return tokens / 1e6 * costPer1M * 100;
|
|
16497
17096
|
}
|
|
17097
|
+
// src/lib/workflow-runner.ts
|
|
17098
|
+
init_database();
|
|
17099
|
+
import { mkdtempSync, rmSync, writeFileSync as writeFileSync3 } from "fs";
|
|
17100
|
+
import { tmpdir } from "os";
|
|
17101
|
+
import { join as join14 } from "path";
|
|
17102
|
+
function buildWorkflowRunPlan(workflow, options) {
|
|
17103
|
+
const runOptions = {
|
|
17104
|
+
url: options.url,
|
|
17105
|
+
model: options.model,
|
|
17106
|
+
headed: options.headed,
|
|
17107
|
+
parallel: options.parallel,
|
|
17108
|
+
timeout: options.timeout ?? workflow.execution.timeoutMs,
|
|
17109
|
+
projectId: workflow.projectId ?? undefined,
|
|
17110
|
+
scenarioIds: workflow.scenarioFilter.scenarioIds,
|
|
17111
|
+
tags: workflow.scenarioFilter.tags,
|
|
17112
|
+
priority: workflow.scenarioFilter.priority,
|
|
17113
|
+
personaIds: workflow.personaIds.length > 0 ? workflow.personaIds : undefined
|
|
17114
|
+
};
|
|
17115
|
+
return {
|
|
17116
|
+
workflow,
|
|
17117
|
+
runOptions,
|
|
17118
|
+
sandbox: workflow.execution.target === "sandbox" ? buildSandboxPlan(workflow, workflow.execution, runOptions) : null
|
|
17119
|
+
};
|
|
17120
|
+
}
|
|
17121
|
+
async function runTestingWorkflow(workflowId, options, dependencies = {}) {
|
|
17122
|
+
const workflow = getTestingWorkflow(workflowId);
|
|
17123
|
+
if (!workflow)
|
|
17124
|
+
throw new Error(`Testing workflow not found: ${workflowId}`);
|
|
17125
|
+
if (!workflow.enabled)
|
|
17126
|
+
throw new Error(`Testing workflow is disabled: ${workflow.name}`);
|
|
17127
|
+
validatePersonaIds(workflow);
|
|
17128
|
+
const plan = buildWorkflowRunPlan(workflow, options);
|
|
17129
|
+
if (options.dryRun)
|
|
17130
|
+
return { run: null, results: [], plan };
|
|
17131
|
+
if (workflow.execution.target === "sandbox") {
|
|
17132
|
+
const sandboxResult = await runViaSandbox(plan, dependencies);
|
|
17133
|
+
return { run: null, results: [], plan, sandboxResult };
|
|
17134
|
+
}
|
|
17135
|
+
const runLocal = dependencies.runByFilter ?? runByFilter;
|
|
17136
|
+
const { run, results } = await runLocal(plan.runOptions);
|
|
17137
|
+
return { run, results, plan };
|
|
17138
|
+
}
|
|
17139
|
+
function createWorkflowDatabaseBundle(workflow, plan) {
|
|
17140
|
+
if (!plan.sandbox)
|
|
17141
|
+
throw new Error(`Workflow is not configured for sandbox execution: ${workflow.name}`);
|
|
17142
|
+
const localDir = mkdtempSync(join14(tmpdir(), `testers-workflow-${workflow.id.slice(0, 8)}-`));
|
|
17143
|
+
writeFileSync3(join14(localDir, "testers.db"), getDatabase().serialize());
|
|
17144
|
+
return {
|
|
17145
|
+
localDir,
|
|
17146
|
+
remoteDir: plan.sandbox.stateRemoteDir,
|
|
17147
|
+
cleanup: () => rmSync(localDir, { recursive: true, force: true })
|
|
17148
|
+
};
|
|
17149
|
+
}
|
|
17150
|
+
function validatePersonaIds(workflow) {
|
|
17151
|
+
for (const personaId of workflow.personaIds) {
|
|
17152
|
+
if (!getPersona(personaId)) {
|
|
17153
|
+
throw new Error(`Persona not found for workflow ${workflow.name}: ${personaId}`);
|
|
17154
|
+
}
|
|
17155
|
+
}
|
|
17156
|
+
}
|
|
17157
|
+
function buildSandboxPlan(workflow, execution, runOptions) {
|
|
17158
|
+
const remoteDir = execution.sandboxRemoteDir ?? `/tmp/testers-workflow-${workflow.id.slice(0, 8)}`;
|
|
17159
|
+
const stateRemoteDir = `${remoteDir.replace(/\/+$/, "")}/.testers-state`;
|
|
17160
|
+
return {
|
|
17161
|
+
provider: execution.provider,
|
|
17162
|
+
image: execution.sandboxImage,
|
|
17163
|
+
name: `testers-${workflow.id.slice(0, 8)}`,
|
|
17164
|
+
remoteDir,
|
|
17165
|
+
stateRemoteDir,
|
|
17166
|
+
cleanup: execution.sandboxCleanup ?? "delete",
|
|
17167
|
+
timeoutMs: execution.timeoutMs,
|
|
17168
|
+
env: execution.env,
|
|
17169
|
+
command: buildSandboxCommand({
|
|
17170
|
+
runOptions,
|
|
17171
|
+
remoteDir,
|
|
17172
|
+
dbPath: `${stateRemoteDir}/testers.db`,
|
|
17173
|
+
setupCommand: execution.setupCommand,
|
|
17174
|
+
packageSpec: execution.packageSpec ?? "@hasna/testers"
|
|
17175
|
+
})
|
|
17176
|
+
};
|
|
17177
|
+
}
|
|
17178
|
+
function buildSandboxCommand(input) {
|
|
17179
|
+
const args = [
|
|
17180
|
+
"bunx",
|
|
17181
|
+
input.packageSpec,
|
|
17182
|
+
"run",
|
|
17183
|
+
input.runOptions.url,
|
|
17184
|
+
...input.runOptions.scenarioIds?.length ? ["--scenario", input.runOptions.scenarioIds.join(",")] : [],
|
|
17185
|
+
...input.runOptions.tags?.length ? input.runOptions.tags.flatMap((tag) => ["--tag", tag]) : [],
|
|
17186
|
+
...input.runOptions.priority ? ["--priority", input.runOptions.priority] : [],
|
|
17187
|
+
...input.runOptions.projectId ? ["--project", input.runOptions.projectId] : [],
|
|
17188
|
+
...input.runOptions.model ? ["--model", input.runOptions.model] : [],
|
|
17189
|
+
...input.runOptions.headed ? ["--headed"] : [],
|
|
17190
|
+
...input.runOptions.parallel ? ["--parallel", String(input.runOptions.parallel)] : [],
|
|
17191
|
+
...input.runOptions.timeout ? ["--timeout", String(input.runOptions.timeout)] : [],
|
|
17192
|
+
...input.runOptions.personaIds?.length ? ["--persona", input.runOptions.personaIds.join(",")] : [],
|
|
17193
|
+
"--no-auto-generate",
|
|
17194
|
+
"--json"
|
|
17195
|
+
];
|
|
17196
|
+
return [
|
|
17197
|
+
"set -euo pipefail",
|
|
17198
|
+
`mkdir -p ${shellQuote(input.remoteDir)}`,
|
|
17199
|
+
`cd ${shellQuote(input.remoteDir)}`,
|
|
17200
|
+
input.setupCommand,
|
|
17201
|
+
`HASNA_TESTERS_DB_PATH=${shellQuote(input.dbPath)} ${args.map(shellQuote).join(" ")}`
|
|
17202
|
+
].filter(Boolean).join(`
|
|
17203
|
+
`);
|
|
17204
|
+
}
|
|
17205
|
+
async function runViaSandbox(plan, dependencies) {
|
|
17206
|
+
if (!plan.sandbox)
|
|
17207
|
+
throw new Error("Workflow does not have a sandbox plan");
|
|
17208
|
+
const sandboxes = await resolveSandboxesRuntime(dependencies);
|
|
17209
|
+
const createBundle = dependencies.createDatabaseBundle ?? createWorkflowDatabaseBundle;
|
|
17210
|
+
const bundle = createBundle(plan.workflow, plan);
|
|
17211
|
+
try {
|
|
17212
|
+
const raw = await sandboxes.runCommandInSandbox({
|
|
17213
|
+
command: plan.sandbox.command,
|
|
17214
|
+
provider: plan.sandbox.provider,
|
|
17215
|
+
name: plan.sandbox.name,
|
|
17216
|
+
image: plan.sandbox.image,
|
|
17217
|
+
sandboxTimeout: plan.sandbox.timeoutMs,
|
|
17218
|
+
commandTimeoutMs: plan.sandbox.timeoutMs,
|
|
17219
|
+
projectId: plan.workflow.projectId ?? undefined,
|
|
17220
|
+
config: {
|
|
17221
|
+
source: "testers",
|
|
17222
|
+
workflowId: plan.workflow.id,
|
|
17223
|
+
workflowName: plan.workflow.name
|
|
17224
|
+
},
|
|
17225
|
+
sandboxEnvVars: plan.sandbox.env,
|
|
17226
|
+
cleanup: plan.sandbox.cleanup,
|
|
17227
|
+
upload: {
|
|
17228
|
+
localDir: bundle.localDir,
|
|
17229
|
+
remoteDir: bundle.remoteDir
|
|
17230
|
+
}
|
|
17231
|
+
});
|
|
17232
|
+
const exitCode = raw.result.exit_code ?? raw.result.exitCode ?? 0;
|
|
17233
|
+
const stdout = raw.result.stdout ?? "";
|
|
17234
|
+
const stderr = raw.result.stderr ?? "";
|
|
17235
|
+
if (exitCode !== 0) {
|
|
17236
|
+
throw new Error(`Sandbox workflow execution failed (${exitCode}): ${stderr || stdout}`);
|
|
17237
|
+
}
|
|
17238
|
+
return {
|
|
17239
|
+
sandboxId: raw.sandbox.id,
|
|
17240
|
+
sessionId: raw.session.id,
|
|
17241
|
+
exitCode,
|
|
17242
|
+
stdout,
|
|
17243
|
+
stderr,
|
|
17244
|
+
cleanup: raw.cleanup
|
|
17245
|
+
};
|
|
17246
|
+
} finally {
|
|
17247
|
+
bundle.cleanup?.();
|
|
17248
|
+
}
|
|
17249
|
+
}
|
|
17250
|
+
async function resolveSandboxesRuntime(dependencies) {
|
|
17251
|
+
if (dependencies.sandboxes)
|
|
17252
|
+
return dependencies.sandboxes;
|
|
17253
|
+
if (dependencies.createSandboxesSDK)
|
|
17254
|
+
return dependencies.createSandboxesSDK();
|
|
17255
|
+
const mod = await import("@hasna/sandboxes");
|
|
17256
|
+
return mod.createSandboxesSDK();
|
|
17257
|
+
}
|
|
17258
|
+
function shellQuote(value) {
|
|
17259
|
+
return `'${value.replaceAll("'", `'"'"'`)}'`;
|
|
17260
|
+
}
|
|
16498
17261
|
// src/lib/reporter.ts
|
|
16499
17262
|
init_database();
|
|
16500
17263
|
function useEmoji() {
|
|
@@ -16668,9 +17431,9 @@ function formatRunList(runs) {
|
|
|
16668
17431
|
`);
|
|
16669
17432
|
}
|
|
16670
17433
|
function getScenarioRunStats(scenarioId) {
|
|
16671
|
-
const
|
|
16672
|
-
const lastRow =
|
|
16673
|
-
const statsRow =
|
|
17434
|
+
const db3 = getDatabase();
|
|
17435
|
+
const lastRow = db3.query("SELECT status FROM results WHERE scenario_id = ? ORDER BY created_at DESC LIMIT 1").get(scenarioId);
|
|
17436
|
+
const statsRow = db3.query("SELECT COUNT(*) as total, SUM(CASE WHEN status = 'passed' THEN 1 ELSE 0 END) as passed FROM results WHERE scenario_id = ?").get(scenarioId);
|
|
16674
17437
|
return {
|
|
16675
17438
|
lastStatus: lastRow ? lastRow.status : null,
|
|
16676
17439
|
passRate: statsRow && statsRow.total > 0 ? `${statsRow.passed}/${statsRow.total}` : "\u2014"
|
|
@@ -16960,10 +17723,10 @@ class Scheduler {
|
|
|
16960
17723
|
}
|
|
16961
17724
|
// src/lib/init.ts
|
|
16962
17725
|
init_paths();
|
|
16963
|
-
import { existsSync as existsSync11, readFileSync as readFileSync3, writeFileSync as
|
|
16964
|
-
import { join as
|
|
17726
|
+
import { existsSync as existsSync11, readFileSync as readFileSync3, writeFileSync as writeFileSync4, mkdirSync as mkdirSync9 } from "fs";
|
|
17727
|
+
import { join as join15, basename } from "path";
|
|
16965
17728
|
function detectFramework(dir) {
|
|
16966
|
-
const pkgPath =
|
|
17729
|
+
const pkgPath = join15(dir, "package.json");
|
|
16967
17730
|
if (!existsSync11(pkgPath))
|
|
16968
17731
|
return null;
|
|
16969
17732
|
let pkg;
|
|
@@ -17191,7 +17954,7 @@ function initProject(options) {
|
|
|
17191
17954
|
}
|
|
17192
17955
|
}).filter((s) => s !== null);
|
|
17193
17956
|
const configDir = getTestersDir();
|
|
17194
|
-
const configPath =
|
|
17957
|
+
const configPath = join15(configDir, "config.json");
|
|
17195
17958
|
if (!existsSync11(configDir)) {
|
|
17196
17959
|
mkdirSync9(configDir, { recursive: true });
|
|
17197
17960
|
}
|
|
@@ -17202,7 +17965,7 @@ function initProject(options) {
|
|
|
17202
17965
|
} catch {}
|
|
17203
17966
|
}
|
|
17204
17967
|
config.activeProject = project.id;
|
|
17205
|
-
|
|
17968
|
+
writeFileSync4(configPath, JSON.stringify(config, null, 2), "utf-8");
|
|
17206
17969
|
return { project, scenarios, framework, url };
|
|
17207
17970
|
}
|
|
17208
17971
|
// src/lib/smoke.ts
|
|
@@ -17630,28 +18393,28 @@ function fromRow2(row) {
|
|
|
17630
18393
|
};
|
|
17631
18394
|
}
|
|
17632
18395
|
function createAuthPreset(input) {
|
|
17633
|
-
const
|
|
18396
|
+
const db3 = getDatabase();
|
|
17634
18397
|
const id = uuid();
|
|
17635
18398
|
const timestamp = now();
|
|
17636
|
-
|
|
18399
|
+
db3.query(`
|
|
17637
18400
|
INSERT INTO auth_presets (id, name, email, password, login_path, metadata, created_at)
|
|
17638
18401
|
VALUES (?, ?, ?, ?, ?, '{}', ?)
|
|
17639
18402
|
`).run(id, input.name, input.email, input.password, input.loginPath ?? "/login", timestamp);
|
|
17640
18403
|
return getAuthPreset(input.name);
|
|
17641
18404
|
}
|
|
17642
18405
|
function getAuthPreset(name) {
|
|
17643
|
-
const
|
|
17644
|
-
const row =
|
|
18406
|
+
const db3 = getDatabase();
|
|
18407
|
+
const row = db3.query("SELECT * FROM auth_presets WHERE name = ?").get(name);
|
|
17645
18408
|
return row ? fromRow2(row) : null;
|
|
17646
18409
|
}
|
|
17647
18410
|
function listAuthPresets() {
|
|
17648
|
-
const
|
|
17649
|
-
const rows =
|
|
18411
|
+
const db3 = getDatabase();
|
|
18412
|
+
const rows = db3.query("SELECT * FROM auth_presets ORDER BY created_at DESC").all();
|
|
17650
18413
|
return rows.map(fromRow2);
|
|
17651
18414
|
}
|
|
17652
18415
|
function deleteAuthPreset(name) {
|
|
17653
|
-
const
|
|
17654
|
-
const result =
|
|
18416
|
+
const db3 = getDatabase();
|
|
18417
|
+
const result = db3.query("DELETE FROM auth_presets WHERE name = ?").run(name);
|
|
17655
18418
|
return result.changes > 0;
|
|
17656
18419
|
}
|
|
17657
18420
|
// src/lib/report.ts
|
|
@@ -17947,12 +18710,12 @@ async function startWatcher(options) {
|
|
|
17947
18710
|
}
|
|
17948
18711
|
// src/lib/repo-discovery.ts
|
|
17949
18712
|
init_paths();
|
|
17950
|
-
import { existsSync as existsSync13, readFileSync as readFileSync5, readdirSync as readdirSync3, statSync, writeFileSync as
|
|
18713
|
+
import { existsSync as existsSync13, readFileSync as readFileSync5, readdirSync as readdirSync3, statSync, writeFileSync as writeFileSync5, mkdirSync as mkdirSync10, unlinkSync } from "fs";
|
|
17951
18714
|
import { createHash } from "crypto";
|
|
17952
|
-
import { join as
|
|
18715
|
+
import { join as join16, resolve as resolve2, relative as relative2 } from "path";
|
|
17953
18716
|
function getCacheDir() {
|
|
17954
18717
|
const testersDir = getTestersDir();
|
|
17955
|
-
const cacheDir =
|
|
18718
|
+
const cacheDir = join16(testersDir, "repo-index");
|
|
17956
18719
|
if (!existsSync13(cacheDir)) {
|
|
17957
18720
|
mkdirSync10(cacheDir, { recursive: true });
|
|
17958
18721
|
}
|
|
@@ -17962,11 +18725,11 @@ function pathHash(repoPath) {
|
|
|
17962
18725
|
return createHash("sha256").update(repoPath).digest("hex").slice(0, 16);
|
|
17963
18726
|
}
|
|
17964
18727
|
function getCachePath(repoPath) {
|
|
17965
|
-
return
|
|
18728
|
+
return join16(getCacheDir(), `${pathHash(repoPath)}.json`);
|
|
17966
18729
|
}
|
|
17967
18730
|
function isCacheStale(cached, repoPath) {
|
|
17968
18731
|
for (const spec of cached.specs) {
|
|
17969
|
-
const fullPath =
|
|
18732
|
+
const fullPath = join16(repoPath, spec.file);
|
|
17970
18733
|
if (!existsSync13(fullPath))
|
|
17971
18734
|
return true;
|
|
17972
18735
|
try {
|
|
@@ -17978,11 +18741,11 @@ function isCacheStale(cached, repoPath) {
|
|
|
17978
18741
|
}
|
|
17979
18742
|
}
|
|
17980
18743
|
if (cached.configPath) {
|
|
17981
|
-
const configFullPath =
|
|
18744
|
+
const configFullPath = join16(repoPath, cached.configPath);
|
|
17982
18745
|
if (!existsSync13(configFullPath))
|
|
17983
18746
|
return true;
|
|
17984
18747
|
try {
|
|
17985
|
-
|
|
18748
|
+
statSync(configFullPath);
|
|
17986
18749
|
const age = Date.now() - new Date(cached.snapshotAt).getTime();
|
|
17987
18750
|
if (age > 3600000)
|
|
17988
18751
|
return true;
|
|
@@ -18005,14 +18768,14 @@ function loadCache(repoPath) {
|
|
|
18005
18768
|
}
|
|
18006
18769
|
function saveCache(snapshot) {
|
|
18007
18770
|
const cachePath = getCachePath(snapshot.repoPath);
|
|
18008
|
-
|
|
18771
|
+
writeFileSync5(cachePath, JSON.stringify(snapshot, null, 2), "utf-8");
|
|
18009
18772
|
}
|
|
18010
18773
|
function detectPackageManager(repoPath) {
|
|
18011
18774
|
const result = {
|
|
18012
|
-
npm: existsSync13(
|
|
18013
|
-
yarn: existsSync13(
|
|
18014
|
-
pnpm: existsSync13(
|
|
18015
|
-
bun: existsSync13(
|
|
18775
|
+
npm: existsSync13(join16(repoPath, "package-lock.json")),
|
|
18776
|
+
yarn: existsSync13(join16(repoPath, "yarn.lock")),
|
|
18777
|
+
pnpm: existsSync13(join16(repoPath, "pnpm-lock.yaml")),
|
|
18778
|
+
bun: existsSync13(join16(repoPath, "bun.lockb")) || existsSync13(join16(repoPath, "bun.lock")),
|
|
18016
18779
|
preferred: "npm"
|
|
18017
18780
|
};
|
|
18018
18781
|
if (result.bun)
|
|
@@ -18026,7 +18789,7 @@ function detectPackageManager(repoPath) {
|
|
|
18026
18789
|
return result;
|
|
18027
18790
|
}
|
|
18028
18791
|
function detectDevScripts(repoPath) {
|
|
18029
|
-
const pkgPath =
|
|
18792
|
+
const pkgPath = join16(repoPath, "package.json");
|
|
18030
18793
|
if (!existsSync13(pkgPath)) {
|
|
18031
18794
|
return { dev: null, test: null, seed: null, build: null };
|
|
18032
18795
|
}
|
|
@@ -18053,7 +18816,7 @@ function findPlaywrightConfig(repoPath) {
|
|
|
18053
18816
|
"playwright-ct.config.js"
|
|
18054
18817
|
];
|
|
18055
18818
|
for (const name of candidates) {
|
|
18056
|
-
if (existsSync13(
|
|
18819
|
+
if (existsSync13(join16(repoPath, name)))
|
|
18057
18820
|
return name;
|
|
18058
18821
|
}
|
|
18059
18822
|
return null;
|
|
@@ -18062,7 +18825,7 @@ function extractTestGlobPatterns(configPath, repoPath) {
|
|
|
18062
18825
|
if (!configPath) {
|
|
18063
18826
|
return ["**/*.spec.ts", "**/*.spec.js", "**/*.test.ts", "**/*.test.js", "**/e2e/**/*.ts", "**/e2e/**/*.js", "**/tests/**/*.ts", "**/tests/**/*.js"];
|
|
18064
18827
|
}
|
|
18065
|
-
const fullPath =
|
|
18828
|
+
const fullPath = join16(repoPath, configPath);
|
|
18066
18829
|
let content;
|
|
18067
18830
|
try {
|
|
18068
18831
|
content = readFileSync5(fullPath, "utf-8");
|
|
@@ -18073,8 +18836,9 @@ function extractTestGlobPatterns(configPath, repoPath) {
|
|
|
18073
18836
|
const testDirMatch = content.match(/testDir\s*[:=]\s*['"`]([^'"`]+)['"`]/);
|
|
18074
18837
|
const testDir = testDirMatch?.[1];
|
|
18075
18838
|
const testMatchArray = content.match(/testMatch\s*[:=]\s*\[([^\]]+)\]/);
|
|
18076
|
-
|
|
18077
|
-
|
|
18839
|
+
const testMatchBody = testMatchArray?.[1];
|
|
18840
|
+
if (testMatchBody) {
|
|
18841
|
+
const items = testMatchBody.match(/['"`]([^'"`]+)['"`]/g);
|
|
18078
18842
|
if (items) {
|
|
18079
18843
|
for (const item of items) {
|
|
18080
18844
|
patterns.push(item.replace(/['"`]/g, ""));
|
|
@@ -18082,8 +18846,9 @@ function extractTestGlobPatterns(configPath, repoPath) {
|
|
|
18082
18846
|
}
|
|
18083
18847
|
}
|
|
18084
18848
|
const testMatchSingle = content.match(/testMatch\s*[:=]\s*['"`]([^'"`]+)['"`]/);
|
|
18085
|
-
|
|
18086
|
-
|
|
18849
|
+
const singleTestMatch = testMatchSingle?.[1];
|
|
18850
|
+
if (singleTestMatch) {
|
|
18851
|
+
patterns.push(singleTestMatch);
|
|
18087
18852
|
}
|
|
18088
18853
|
if (testDir && patterns.length === 0) {
|
|
18089
18854
|
patterns.push(`${testDir}/**/*.spec.ts`, `${testDir}/**/*.test.ts`, `${testDir}/**/*.spec.js`, `${testDir}/**/*.test.js`);
|
|
@@ -18109,7 +18874,7 @@ function findSpecFiles(repoPath, globPatterns) {
|
|
|
18109
18874
|
for (const pattern of globPatterns) {
|
|
18110
18875
|
const dirsToSearch = ["", ".", "tests", "e2e", "test", "__tests__", "specs", "src"];
|
|
18111
18876
|
for (const dir of dirsToSearch) {
|
|
18112
|
-
const searchDir = dir ?
|
|
18877
|
+
const searchDir = dir ? join16(repoPath, dir) : repoPath;
|
|
18113
18878
|
if (!existsSync13(searchDir))
|
|
18114
18879
|
continue;
|
|
18115
18880
|
try {
|
|
@@ -18143,7 +18908,7 @@ function walkDir(dir) {
|
|
|
18143
18908
|
try {
|
|
18144
18909
|
const entries = readdirSync3(dir, { withFileTypes: true });
|
|
18145
18910
|
for (const entry of entries) {
|
|
18146
|
-
const fullPath =
|
|
18911
|
+
const fullPath = join16(dir, entry.name);
|
|
18147
18912
|
if (entry.isDirectory()) {
|
|
18148
18913
|
if (entry.name === "node_modules" || entry.name === ".git")
|
|
18149
18914
|
continue;
|
|
@@ -18161,7 +18926,7 @@ function matchesGlob(filePath, pattern) {
|
|
|
18161
18926
|
return new RegExp(regex).test(filePath);
|
|
18162
18927
|
}
|
|
18163
18928
|
function detectSuggestedUrl(repoPath) {
|
|
18164
|
-
const pkgPath =
|
|
18929
|
+
const pkgPath = join16(repoPath, "package.json");
|
|
18165
18930
|
if (!existsSync13(pkgPath))
|
|
18166
18931
|
return null;
|
|
18167
18932
|
try {
|
|
@@ -18181,10 +18946,10 @@ function detectSuggestedUrl(repoPath) {
|
|
|
18181
18946
|
return null;
|
|
18182
18947
|
}
|
|
18183
18948
|
function checkPlaywrightBrowserInstalled(repoPath) {
|
|
18184
|
-
const cacheDir =
|
|
18949
|
+
const cacheDir = join16(repoPath, "node_modules", ".cache", "ms-playwright");
|
|
18185
18950
|
if (existsSync13(cacheDir))
|
|
18186
18951
|
return true;
|
|
18187
|
-
const globalCache =
|
|
18952
|
+
const globalCache = join16(repoPath, ".cache", "ms-playwright");
|
|
18188
18953
|
if (existsSync13(globalCache))
|
|
18189
18954
|
return true;
|
|
18190
18955
|
return false;
|
|
@@ -18201,7 +18966,7 @@ function getInstallCommand(pm) {
|
|
|
18201
18966
|
return "bun install";
|
|
18202
18967
|
}
|
|
18203
18968
|
}
|
|
18204
|
-
function getPlaywrightInstallCommand(
|
|
18969
|
+
function getPlaywrightInstallCommand(_pm) {
|
|
18205
18970
|
return "npx playwright install";
|
|
18206
18971
|
}
|
|
18207
18972
|
function discoverRepo(opts) {
|
|
@@ -18216,7 +18981,7 @@ function discoverRepo(opts) {
|
|
|
18216
18981
|
let configRaw = null;
|
|
18217
18982
|
if (configPath) {
|
|
18218
18983
|
try {
|
|
18219
|
-
configRaw = readFileSync5(
|
|
18984
|
+
configRaw = readFileSync5(join16(repoPath, configPath), "utf-8");
|
|
18220
18985
|
} catch {
|
|
18221
18986
|
configRaw = null;
|
|
18222
18987
|
}
|
|
@@ -18225,7 +18990,7 @@ function discoverRepo(opts) {
|
|
|
18225
18990
|
const specs = findSpecFiles(repoPath, globPatterns);
|
|
18226
18991
|
const packageManager = detectPackageManager(repoPath);
|
|
18227
18992
|
const devScripts = detectDevScripts(repoPath);
|
|
18228
|
-
const playwrightInstalled = existsSync13(
|
|
18993
|
+
const playwrightInstalled = existsSync13(join16(repoPath, "node_modules", "playwright")) || existsSync13(join16(repoPath, "node_modules", "@playwright", "test"));
|
|
18229
18994
|
const browsersInstalled = checkPlaywrightBrowserInstalled(repoPath);
|
|
18230
18995
|
const configExists = configPath !== null;
|
|
18231
18996
|
const specsFound = specs.length > 0;
|
|
@@ -18294,7 +19059,7 @@ function clearDiscoveryCache(repoPath) {
|
|
|
18294
19059
|
} else {
|
|
18295
19060
|
for (const file of readdirSync3(cacheDir)) {
|
|
18296
19061
|
if (file.endsWith(".json")) {
|
|
18297
|
-
unlinkSync(
|
|
19062
|
+
unlinkSync(join16(cacheDir, file));
|
|
18298
19063
|
}
|
|
18299
19064
|
}
|
|
18300
19065
|
}
|
|
@@ -18317,10 +19082,10 @@ init_runs();
|
|
|
18317
19082
|
init_database();
|
|
18318
19083
|
init_paths();
|
|
18319
19084
|
import { execSync as execSync2 } from "child_process";
|
|
18320
|
-
import { existsSync as existsSync14, mkdirSync as mkdirSync11, writeFileSync as
|
|
18321
|
-
import { join as
|
|
19085
|
+
import { existsSync as existsSync14, mkdirSync as mkdirSync11, writeFileSync as writeFileSync6 } from "fs";
|
|
19086
|
+
import { join as join17 } from "path";
|
|
18322
19087
|
function resolvePlaywrightCmd(repoPath) {
|
|
18323
|
-
const localPw =
|
|
19088
|
+
const localPw = join17(repoPath, "node_modules", ".bin", "playwright");
|
|
18324
19089
|
if (existsSync14(localPw)) {
|
|
18325
19090
|
return [localPw, "test"];
|
|
18326
19091
|
}
|
|
@@ -18339,7 +19104,7 @@ function buildPlaywrightArgs(specFiles, extraArgs = []) {
|
|
|
18339
19104
|
}
|
|
18340
19105
|
function runPlaywright(repoPath, workingDir, specFiles, extraArgs, timeoutMs) {
|
|
18341
19106
|
const cmd = resolvePlaywrightCmd(repoPath);
|
|
18342
|
-
const args = buildPlaywrightArgs(specFiles, extraArgs
|
|
19107
|
+
const args = buildPlaywrightArgs(specFiles, extraArgs);
|
|
18343
19108
|
const startTime = Date.now();
|
|
18344
19109
|
try {
|
|
18345
19110
|
const result = execSync2(`${cmd.join(" ")} ${args.join(" ")}`, {
|
|
@@ -18367,7 +19132,7 @@ function runPlaywright(repoPath, workingDir, specFiles, extraArgs, timeoutMs) {
|
|
|
18367
19132
|
};
|
|
18368
19133
|
}
|
|
18369
19134
|
}
|
|
18370
|
-
function parsePlaywrightJsonOutput(stdout,
|
|
19135
|
+
function parsePlaywrightJsonOutput(stdout, _stderr) {
|
|
18371
19136
|
const testResults = [];
|
|
18372
19137
|
try {
|
|
18373
19138
|
const obj = JSON.parse(stdout);
|
|
@@ -18472,19 +19237,21 @@ async function runRepoTests(opts) {
|
|
|
18472
19237
|
const workingDir = opts.snapshot.workingDir;
|
|
18473
19238
|
const repoPath = snapshot.repoPath;
|
|
18474
19239
|
const url = opts.url ?? snapshot.suggestedUrl ?? "http://localhost:3000";
|
|
18475
|
-
const
|
|
19240
|
+
const initialRun = createRun({
|
|
18476
19241
|
projectId: opts.projectId,
|
|
18477
19242
|
url,
|
|
18478
19243
|
model: opts.model ?? "repo-native",
|
|
18479
19244
|
headed: false,
|
|
18480
|
-
parallel: 1
|
|
18481
|
-
|
|
19245
|
+
parallel: 1
|
|
19246
|
+
});
|
|
19247
|
+
const run = updateRun(initialRun.id, {
|
|
19248
|
+
metadata: JSON.stringify({
|
|
18482
19249
|
runType: "repo-native",
|
|
18483
19250
|
repoPath,
|
|
18484
19251
|
configPath: snapshot.configPath,
|
|
18485
19252
|
cacheKey: snapshot.cacheKey,
|
|
18486
19253
|
label: opts.label
|
|
18487
|
-
}
|
|
19254
|
+
})
|
|
18488
19255
|
});
|
|
18489
19256
|
const specResults = [];
|
|
18490
19257
|
const startTime = Date.now();
|
|
@@ -18496,12 +19263,12 @@ async function runRepoTests(opts) {
|
|
|
18496
19263
|
specResults.push(result);
|
|
18497
19264
|
const resultId = uuid();
|
|
18498
19265
|
const timestamp = now();
|
|
18499
|
-
const
|
|
18500
|
-
|
|
19266
|
+
const db3 = getDatabase();
|
|
19267
|
+
db3.exec("PRAGMA foreign_keys = OFF");
|
|
18501
19268
|
try {
|
|
18502
19269
|
const reasoning = result.status === "passed" ? "All tests passed" : (result.error ?? "").slice(0, 500) || null;
|
|
18503
19270
|
const errorStr = result.status !== "passed" ? result.error ?? null : null;
|
|
18504
|
-
|
|
19271
|
+
db3.query(`
|
|
18505
19272
|
INSERT INTO results (id, run_id, scenario_id, status, reasoning, error, steps_completed, steps_total, duration_ms, model, tokens_used, cost_cents, metadata, created_at, persona_id, persona_name)
|
|
18506
19273
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0, 0, ?, ?, NULL, NULL)
|
|
18507
19274
|
`).run(resultId, run.id, "__repo__", result.status, reasoning, errorStr, result.testResults.filter((t) => t.status === "passed").length, result.testResults.length || 1, result.durationMs, "repo-native", JSON.stringify({
|
|
@@ -18510,14 +19277,14 @@ async function runRepoTests(opts) {
|
|
|
18510
19277
|
testResults: result.testResults
|
|
18511
19278
|
}), timestamp);
|
|
18512
19279
|
} finally {
|
|
18513
|
-
|
|
19280
|
+
db3.exec("PRAGMA foreign_keys = ON");
|
|
18514
19281
|
}
|
|
18515
19282
|
const resultRecord = { id: resultId };
|
|
18516
19283
|
if (result.stdout || result.stderr) {
|
|
18517
|
-
const reportersDir =
|
|
19284
|
+
const reportersDir = join17(getTestersDir(), "repo-run-output");
|
|
18518
19285
|
mkdirSync11(reportersDir, { recursive: true });
|
|
18519
|
-
const outputFile =
|
|
18520
|
-
|
|
19286
|
+
const outputFile = join17(reportersDir, `${resultRecord.id}.log`);
|
|
19287
|
+
writeFileSync6(outputFile, `=== stdout ===
|
|
18521
19288
|
${result.stdout}
|
|
18522
19289
|
|
|
18523
19290
|
=== stderr ===
|
|
@@ -19122,46 +19889,46 @@ async function postGitHubComment(run, results, options) {
|
|
|
19122
19889
|
// src/db/sessions.ts
|
|
19123
19890
|
init_database();
|
|
19124
19891
|
function createSession(input) {
|
|
19125
|
-
const
|
|
19892
|
+
const db3 = getDatabase();
|
|
19126
19893
|
const id = input.sessionId ?? uuid();
|
|
19127
19894
|
const timestamp = now();
|
|
19128
|
-
|
|
19895
|
+
db3.query(`
|
|
19129
19896
|
INSERT INTO sessions (id, tab_id, url, title, entries, entry_count, error_count, console_count, nav_count, status, start_time, end_time, created_at)
|
|
19130
19897
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
19131
19898
|
`).run(id, input.tabId, input.url ?? null, input.title ?? null, input.entries, input.entryCount, input.errorCount ?? 0, input.consoleCount ?? 0, input.navCount ?? 0, input.status, input.startTime, input.endTime ?? null, timestamp);
|
|
19132
19899
|
return getSession(id);
|
|
19133
19900
|
}
|
|
19134
19901
|
function getSession(id) {
|
|
19135
|
-
const
|
|
19136
|
-
let row =
|
|
19902
|
+
const db3 = getDatabase();
|
|
19903
|
+
let row = db3.query("SELECT * FROM sessions WHERE id = ?").get(id);
|
|
19137
19904
|
if (row)
|
|
19138
19905
|
return sessionFromRow(row);
|
|
19139
19906
|
const fullId = resolvePartialId("sessions", id);
|
|
19140
19907
|
if (fullId) {
|
|
19141
|
-
row =
|
|
19908
|
+
row = db3.query("SELECT * FROM sessions WHERE id = ?").get(fullId);
|
|
19142
19909
|
if (row)
|
|
19143
19910
|
return sessionFromRow(row);
|
|
19144
19911
|
}
|
|
19145
19912
|
return null;
|
|
19146
19913
|
}
|
|
19147
19914
|
function listSessions(limit = 50, offset = 0) {
|
|
19148
|
-
const
|
|
19149
|
-
const rows =
|
|
19915
|
+
const db3 = getDatabase();
|
|
19916
|
+
const rows = db3.query("SELECT * FROM sessions ORDER BY created_at DESC LIMIT ? OFFSET ?").all(limit, offset);
|
|
19150
19917
|
return rows.map(sessionFromRow);
|
|
19151
19918
|
}
|
|
19152
19919
|
function deleteSession(id) {
|
|
19153
|
-
const
|
|
19154
|
-
const result =
|
|
19920
|
+
const db3 = getDatabase();
|
|
19921
|
+
const result = db3.query("DELETE FROM sessions WHERE id = ?").run(id);
|
|
19155
19922
|
return result.changes > 0;
|
|
19156
19923
|
}
|
|
19157
19924
|
function searchSessions(query, limit = 20) {
|
|
19158
|
-
const
|
|
19159
|
-
const rows =
|
|
19925
|
+
const db3 = getDatabase();
|
|
19926
|
+
const rows = db3.query("SELECT * FROM sessions WHERE url LIKE ? OR title LIKE ? ORDER BY created_at DESC LIMIT ?").all(`%${query}%`, `%${query}%`, limit);
|
|
19160
19927
|
return rows.map(sessionFromRow);
|
|
19161
19928
|
}
|
|
19162
19929
|
function countSessions() {
|
|
19163
|
-
const
|
|
19164
|
-
const row =
|
|
19930
|
+
const db3 = getDatabase();
|
|
19931
|
+
const row = db3.query("SELECT COUNT(*) as count FROM sessions").get();
|
|
19165
19932
|
return row.count;
|
|
19166
19933
|
}
|
|
19167
19934
|
function sessionFromRow(row) {
|
|
@@ -19185,6 +19952,7 @@ export {
|
|
|
19185
19952
|
writeScenarioMeta,
|
|
19186
19953
|
writeRunMeta,
|
|
19187
19954
|
uuid,
|
|
19955
|
+
updateTestingWorkflow,
|
|
19188
19956
|
updateSchedule,
|
|
19189
19957
|
updateScenario,
|
|
19190
19958
|
updateRun,
|
|
@@ -19202,6 +19970,7 @@ export {
|
|
|
19202
19970
|
screenshotFromRow,
|
|
19203
19971
|
scheduleFromRow,
|
|
19204
19972
|
scenarioFromRow,
|
|
19973
|
+
runTestingWorkflow,
|
|
19205
19974
|
runSmoke,
|
|
19206
19975
|
runSingleScenario,
|
|
19207
19976
|
runRepoTests,
|
|
@@ -19233,6 +20002,7 @@ export {
|
|
|
19233
20002
|
loginWithAuthConfig,
|
|
19234
20003
|
loadConfig,
|
|
19235
20004
|
listWebhooks,
|
|
20005
|
+
listTestingWorkflows,
|
|
19236
20006
|
listTemplateNames,
|
|
19237
20007
|
listSessions,
|
|
19238
20008
|
listScreenshots,
|
|
@@ -19255,6 +20025,7 @@ export {
|
|
|
19255
20025
|
imageToBase64,
|
|
19256
20026
|
getWebhook,
|
|
19257
20027
|
getTransitiveDependencies,
|
|
20028
|
+
getTestingWorkflow,
|
|
19258
20029
|
getTemplate,
|
|
19259
20030
|
getStarterScenarios,
|
|
19260
20031
|
getSession,
|
|
@@ -19311,13 +20082,16 @@ export {
|
|
|
19311
20082
|
diffRuns,
|
|
19312
20083
|
detectFramework,
|
|
19313
20084
|
deleteWebhook,
|
|
20085
|
+
deleteTestingWorkflow,
|
|
19314
20086
|
deleteSession,
|
|
19315
20087
|
deleteSchedule,
|
|
19316
20088
|
deleteScenario,
|
|
19317
20089
|
deleteRun,
|
|
19318
20090
|
deleteFlow,
|
|
19319
20091
|
deleteAuthPreset,
|
|
20092
|
+
createWorkflowDatabaseBundle,
|
|
19320
20093
|
createWebhook,
|
|
20094
|
+
createTestingWorkflow,
|
|
19321
20095
|
createSession,
|
|
19322
20096
|
createScreenshot,
|
|
19323
20097
|
createSchedule,
|
|
@@ -19336,6 +20110,7 @@ export {
|
|
|
19336
20110
|
closeBrowser,
|
|
19337
20111
|
clearDiscoveryCache,
|
|
19338
20112
|
checkBudget,
|
|
20113
|
+
buildWorkflowRunPlan,
|
|
19339
20114
|
agentFromRow,
|
|
19340
20115
|
addDependency,
|
|
19341
20116
|
VersionConflictError,
|