@hasna/testers 0.0.8 → 0.0.10
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/dashboard/dist/assets/index-DvYdwJK-.css +1 -0
- package/dashboard/dist/assets/index-RV9LMdfY.js +49 -0
- package/dashboard/dist/index.html +2 -2
- package/dist/cli/index.js +1191 -354
- package/dist/db/results.d.ts +1 -0
- package/dist/db/results.d.ts.map +1 -1
- package/dist/db/runs.d.ts +1 -0
- package/dist/db/runs.d.ts.map +1 -1
- package/dist/db/scenarios.d.ts +1 -0
- package/dist/db/scenarios.d.ts.map +1 -1
- package/dist/db/screenshots.d.ts +1 -0
- package/dist/db/screenshots.d.ts.map +1 -1
- package/dist/index.js +256 -21
- package/dist/lib/costs.d.ts +1 -0
- package/dist/lib/costs.d.ts.map +1 -1
- package/dist/lib/init.d.ts.map +1 -1
- package/dist/lib/logs-integration.d.ts +7 -0
- package/dist/lib/logs-integration.d.ts.map +1 -0
- package/dist/lib/reporter.d.ts +7 -0
- package/dist/lib/reporter.d.ts.map +1 -1
- package/dist/lib/runner.d.ts +3 -0
- package/dist/lib/runner.d.ts.map +1 -1
- package/dist/mcp/index.js +45 -2
- package/dist/server/index.js +4172 -16
- package/package.json +1 -1
- package/dashboard/dist/assets/index-CDcHt94n.css +0 -1
- package/dashboard/dist/assets/index-DCNDCh61.js +0 -49
package/dist/db/results.d.ts
CHANGED
|
@@ -17,4 +17,5 @@ export declare function updateResult(id: string, updates: Partial<{
|
|
|
17
17
|
costCents: number;
|
|
18
18
|
}>): Result;
|
|
19
19
|
export declare function getResultsByRun(runId: string): Result[];
|
|
20
|
+
export declare function countResultsByRun(runId: string): number;
|
|
20
21
|
//# sourceMappingURL=results.d.ts.map
|
package/dist/db/results.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"results.d.ts","sourceRoot":"","sources":["../../src/db/results.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,MAAM,EAEX,KAAK,YAAY,EAElB,MAAM,mBAAmB,CAAC;AAG3B,wBAAgB,YAAY,CAAC,KAAK,EAAE;IAClC,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;CACpB,GAAG,MAAM,CAkBT;AAED,wBAAgB,SAAS,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAanD;AAED,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CAMnD;AAED,wBAAgB,YAAY,CAC1B,EAAE,EAAE,MAAM,EACV,OAAO,EAAE,OAAO,CAAC;IACf,MAAM,EAAE,YAAY,CAAC;IACrB,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,cAAc,EAAE,MAAM,CAAC;IACvB,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;CACnB,CAAC,GACD,MAAM,CA+CR;AAED,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CAEvD"}
|
|
1
|
+
{"version":3,"file":"results.d.ts","sourceRoot":"","sources":["../../src/db/results.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,MAAM,EAEX,KAAK,YAAY,EAElB,MAAM,mBAAmB,CAAC;AAG3B,wBAAgB,YAAY,CAAC,KAAK,EAAE;IAClC,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;CACpB,GAAG,MAAM,CAkBT;AAED,wBAAgB,SAAS,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAanD;AAED,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CAMnD;AAED,wBAAgB,YAAY,CAC1B,EAAE,EAAE,MAAM,EACV,OAAO,EAAE,OAAO,CAAC;IACf,MAAM,EAAE,YAAY,CAAC;IACrB,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,cAAc,EAAE,MAAM,CAAC;IACvB,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;CACnB,CAAC,GACD,MAAM,CA+CR;AAED,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CAEvD;AAED,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAMvD"}
|
package/dist/db/runs.d.ts
CHANGED
|
@@ -4,6 +4,7 @@ export declare function createRun(input: CreateRunInput & {
|
|
|
4
4
|
}): Run;
|
|
5
5
|
export declare function getRun(id: string): Run | null;
|
|
6
6
|
export declare function listRuns(filter?: RunFilter): Run[];
|
|
7
|
+
export declare function countRuns(filter?: RunFilter): number;
|
|
7
8
|
export declare function updateRun(id: string, updates: Partial<RunRow>): Run;
|
|
8
9
|
export declare function deleteRun(id: string): boolean;
|
|
9
10
|
//# sourceMappingURL=runs.d.ts.map
|
package/dist/db/runs.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"runs.d.ts","sourceRoot":"","sources":["../../src/db/runs.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,GAAG,EACR,KAAK,MAAM,EACX,KAAK,cAAc,EACnB,KAAK,SAAS,EAEf,MAAM,mBAAmB,CAAC;AAG3B,wBAAgB,SAAS,CAAC,KAAK,EAAE,cAAc,GAAG;IAAE,KAAK,EAAE,MAAM,CAAA;CAAE,GAAG,GAAG,CAoBxE;AAED,wBAAgB,MAAM,CAAC,EAAE,EAAE,MAAM,GAAG,GAAG,GAAG,IAAI,CAc7C;AAED,wBAAgB,QAAQ,CAAC,MAAM,CAAC,EAAE,SAAS,GAAG,GAAG,EAAE,CAgClD;AAED,wBAAgB,SAAS,CAAC,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,CAAC,MAAM,CAAC,GAAG,GAAG,CAmEnE;AAED,wBAAgB,SAAS,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAO7C"}
|
|
1
|
+
{"version":3,"file":"runs.d.ts","sourceRoot":"","sources":["../../src/db/runs.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,GAAG,EACR,KAAK,MAAM,EACX,KAAK,cAAc,EACnB,KAAK,SAAS,EAEf,MAAM,mBAAmB,CAAC;AAG3B,wBAAgB,SAAS,CAAC,KAAK,EAAE,cAAc,GAAG;IAAE,KAAK,EAAE,MAAM,CAAA;CAAE,GAAG,GAAG,CAoBxE;AAED,wBAAgB,MAAM,CAAC,EAAE,EAAE,MAAM,GAAG,GAAG,GAAG,IAAI,CAc7C;AAED,wBAAgB,QAAQ,CAAC,MAAM,CAAC,EAAE,SAAS,GAAG,GAAG,EAAE,CAgClD;AAED,wBAAgB,SAAS,CAAC,MAAM,CAAC,EAAE,SAAS,GAAG,MAAM,CAmBpD;AAED,wBAAgB,SAAS,CAAC,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,CAAC,MAAM,CAAC,GAAG,GAAG,CAmEnE;AAED,wBAAgB,SAAS,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAO7C"}
|
package/dist/db/scenarios.d.ts
CHANGED
|
@@ -4,5 +4,6 @@ export declare function getScenario(id: string): Scenario | null;
|
|
|
4
4
|
export declare function getScenarioByShortId(shortId: string): Scenario | null;
|
|
5
5
|
export declare function listScenarios(filter?: ScenarioFilter): Scenario[];
|
|
6
6
|
export declare function updateScenario(id: string, input: UpdateScenarioInput, version: number): Scenario;
|
|
7
|
+
export declare function countScenarios(filter?: ScenarioFilter): number;
|
|
7
8
|
export declare function deleteScenario(id: string): boolean;
|
|
8
9
|
//# sourceMappingURL=scenarios.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"scenarios.d.ts","sourceRoot":"","sources":["../../src/db/scenarios.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,QAAQ,EAEb,KAAK,mBAAmB,EACxB,KAAK,mBAAmB,EACxB,KAAK,cAAc,EAGpB,MAAM,mBAAmB,CAAC;AAsB3B,wBAAgB,cAAc,CAAC,KAAK,EAAE,mBAAmB,GAAG,QAAQ,CA8BnE;AAED,wBAAgB,WAAW,CAAC,EAAE,EAAE,MAAM,GAAG,QAAQ,GAAG,IAAI,CAmBvD;AAED,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,MAAM,GAAG,QAAQ,GAAG,IAAI,CAIrE;AAED,wBAAgB,aAAa,CAAC,MAAM,CAAC,EAAE,cAAc,GAAG,QAAQ,EAAE,CA6CjE;AAED,wBAAgB,cAAc,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,mBAAmB,EAAE,OAAO,EAAE,MAAM,GAAG,QAAQ,CAoFhG;AAED,wBAAgB,cAAc,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAOlD"}
|
|
1
|
+
{"version":3,"file":"scenarios.d.ts","sourceRoot":"","sources":["../../src/db/scenarios.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,QAAQ,EAEb,KAAK,mBAAmB,EACxB,KAAK,mBAAmB,EACxB,KAAK,cAAc,EAGpB,MAAM,mBAAmB,CAAC;AAsB3B,wBAAgB,cAAc,CAAC,KAAK,EAAE,mBAAmB,GAAG,QAAQ,CA8BnE;AAED,wBAAgB,WAAW,CAAC,EAAE,EAAE,MAAM,GAAG,QAAQ,GAAG,IAAI,CAmBvD;AAED,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,MAAM,GAAG,QAAQ,GAAG,IAAI,CAIrE;AAED,wBAAgB,aAAa,CAAC,MAAM,CAAC,EAAE,cAAc,GAAG,QAAQ,EAAE,CA6CjE;AAED,wBAAgB,cAAc,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,mBAAmB,EAAE,OAAO,EAAE,MAAM,GAAG,QAAQ,CAoFhG;AAED,wBAAgB,cAAc,CAAC,MAAM,CAAC,EAAE,cAAc,GAAG,MAAM,CA8B9D;AAED,wBAAgB,cAAc,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAOlD"}
|
package/dist/db/screenshots.d.ts
CHANGED
|
@@ -13,4 +13,5 @@ export declare function createScreenshot(input: {
|
|
|
13
13
|
export declare function getScreenshot(id: string): Screenshot | null;
|
|
14
14
|
export declare function listScreenshots(resultId: string): Screenshot[];
|
|
15
15
|
export declare function getScreenshotsByResult(resultId: string): Screenshot[];
|
|
16
|
+
export declare function countScreenshots(resultId: string): number;
|
|
16
17
|
//# sourceMappingURL=screenshots.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"screenshots.d.ts","sourceRoot":"","sources":["../../src/db/screenshots.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,UAAU,EAGhB,MAAM,mBAAmB,CAAC;AAG3B,wBAAgB,gBAAgB,CAAC,KAAK,EAAE;IACtC,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC/B,GAAG,UAAU,CAuBb;AAED,wBAAgB,aAAa,CAAC,EAAE,EAAE,MAAM,GAAG,UAAU,GAAG,IAAI,CAI3D;AAED,wBAAgB,eAAe,CAAC,QAAQ,EAAE,MAAM,GAAG,UAAU,EAAE,CAM9D;AAED,wBAAgB,sBAAsB,CAAC,QAAQ,EAAE,MAAM,GAAG,UAAU,EAAE,CAErE"}
|
|
1
|
+
{"version":3,"file":"screenshots.d.ts","sourceRoot":"","sources":["../../src/db/screenshots.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,UAAU,EAGhB,MAAM,mBAAmB,CAAC;AAG3B,wBAAgB,gBAAgB,CAAC,KAAK,EAAE;IACtC,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC/B,GAAG,UAAU,CAuBb;AAED,wBAAgB,aAAa,CAAC,EAAE,EAAE,MAAM,GAAG,UAAU,GAAG,IAAI,CAI3D;AAED,wBAAgB,eAAe,CAAC,QAAQ,EAAE,MAAM,GAAG,UAAU,EAAE,CAM9D;AAED,wBAAgB,sBAAsB,CAAC,QAAQ,EAAE,MAAM,GAAG,UAAU,EAAE,CAErE;AAED,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAMzD"}
|
package/dist/index.js
CHANGED
|
@@ -513,7 +513,8 @@ __export(exports_runs, {
|
|
|
513
513
|
listRuns: () => listRuns,
|
|
514
514
|
getRun: () => getRun,
|
|
515
515
|
deleteRun: () => deleteRun,
|
|
516
|
-
createRun: () => createRun
|
|
516
|
+
createRun: () => createRun,
|
|
517
|
+
countRuns: () => countRuns
|
|
517
518
|
});
|
|
518
519
|
function createRun(input) {
|
|
519
520
|
const db2 = getDatabase();
|
|
@@ -566,6 +567,24 @@ function listRuns(filter) {
|
|
|
566
567
|
const rows = db2.query(sql).all(...params);
|
|
567
568
|
return rows.map(runFromRow);
|
|
568
569
|
}
|
|
570
|
+
function countRuns(filter) {
|
|
571
|
+
const db2 = getDatabase();
|
|
572
|
+
const conditions = [];
|
|
573
|
+
const params = [];
|
|
574
|
+
if (filter?.projectId) {
|
|
575
|
+
conditions.push("project_id = ?");
|
|
576
|
+
params.push(filter.projectId);
|
|
577
|
+
}
|
|
578
|
+
if (filter?.status) {
|
|
579
|
+
conditions.push("status = ?");
|
|
580
|
+
params.push(filter.status);
|
|
581
|
+
}
|
|
582
|
+
let sql = "SELECT COUNT(*) as count FROM runs";
|
|
583
|
+
if (conditions.length > 0)
|
|
584
|
+
sql += " WHERE " + conditions.join(" AND ");
|
|
585
|
+
const row = db2.query(sql).get(...params);
|
|
586
|
+
return row.count;
|
|
587
|
+
}
|
|
569
588
|
function updateRun(id, updates) {
|
|
570
589
|
const db2 = getDatabase();
|
|
571
590
|
const existing = getRun(id);
|
|
@@ -2665,6 +2684,38 @@ async function testWebhook(id) {
|
|
|
2665
2684
|
}
|
|
2666
2685
|
}
|
|
2667
2686
|
|
|
2687
|
+
// src/lib/logs-integration.ts
|
|
2688
|
+
async function pushFailedRunToLogs(run, failedResults, scenarios) {
|
|
2689
|
+
const logsUrl = process.env.LOGS_URL;
|
|
2690
|
+
if (!logsUrl)
|
|
2691
|
+
return;
|
|
2692
|
+
const scenarioMap = new Map(scenarios.map((s) => [s.id, s]));
|
|
2693
|
+
const entries = failedResults.map((result) => {
|
|
2694
|
+
const scenario = scenarioMap.get(result.scenarioId);
|
|
2695
|
+
return {
|
|
2696
|
+
level: "error",
|
|
2697
|
+
source: "sdk",
|
|
2698
|
+
service: "testers",
|
|
2699
|
+
message: `[testers] Scenario failed: ${scenario?.name ?? result.scenarioId}${result.error ? ` \u2014 ${result.error}` : ""}`,
|
|
2700
|
+
metadata: {
|
|
2701
|
+
run_id: run.id,
|
|
2702
|
+
scenario_id: result.scenarioId,
|
|
2703
|
+
scenario_name: scenario?.name,
|
|
2704
|
+
url: run.url,
|
|
2705
|
+
status: result.status,
|
|
2706
|
+
duration_ms: result.durationMs
|
|
2707
|
+
}
|
|
2708
|
+
};
|
|
2709
|
+
});
|
|
2710
|
+
try {
|
|
2711
|
+
await fetch(`${logsUrl.replace(/\/$/, "")}/api/logs`, {
|
|
2712
|
+
method: "POST",
|
|
2713
|
+
headers: { "Content-Type": "application/json" },
|
|
2714
|
+
body: JSON.stringify(entries)
|
|
2715
|
+
});
|
|
2716
|
+
} catch {}
|
|
2717
|
+
}
|
|
2718
|
+
|
|
2668
2719
|
// src/lib/runner.ts
|
|
2669
2720
|
var eventHandler = null;
|
|
2670
2721
|
function onRunEvent(handler) {
|
|
@@ -2677,7 +2728,7 @@ function emit(event) {
|
|
|
2677
2728
|
function withTimeout(promise, ms, label) {
|
|
2678
2729
|
return new Promise((resolve, reject) => {
|
|
2679
2730
|
const timer = setTimeout(() => {
|
|
2680
|
-
reject(new Error(`Scenario
|
|
2731
|
+
reject(new Error(`Scenario '${label}' timed out after ${ms}ms. Try: testers run --timeout ${ms * 2} or simplify the scenario steps.`));
|
|
2681
2732
|
}, ms);
|
|
2682
2733
|
promise.then((val) => {
|
|
2683
2734
|
clearTimeout(timer);
|
|
@@ -2810,6 +2861,7 @@ async function runBatch(scenarios, options) {
|
|
|
2810
2861
|
} catch {}
|
|
2811
2862
|
return true;
|
|
2812
2863
|
};
|
|
2864
|
+
const maxRetries = options.retry ?? 0;
|
|
2813
2865
|
if (parallel <= 1) {
|
|
2814
2866
|
for (const scenario of sortedScenarios) {
|
|
2815
2867
|
if (!await canRun(scenario)) {
|
|
@@ -2820,7 +2872,13 @@ async function runBatch(scenarios, options) {
|
|
|
2820
2872
|
emit({ type: "scenario:error", scenarioId: scenario.id, scenarioName: scenario.name, error: "Dependency failed \u2014 skipped", runId: run.id });
|
|
2821
2873
|
continue;
|
|
2822
2874
|
}
|
|
2823
|
-
|
|
2875
|
+
let result = await runSingleScenario(scenario, run.id, options);
|
|
2876
|
+
let attempt = 1;
|
|
2877
|
+
while ((result.status === "failed" || result.status === "error") && attempt <= maxRetries) {
|
|
2878
|
+
emit({ type: "scenario:start", scenarioId: scenario.id, scenarioName: scenario.name, runId: run.id, retryAttempt: attempt + 1, maxRetries: maxRetries + 1 });
|
|
2879
|
+
result = await runSingleScenario(scenario, run.id, options);
|
|
2880
|
+
attempt++;
|
|
2881
|
+
}
|
|
2824
2882
|
results.push(result);
|
|
2825
2883
|
if (result.status === "failed" || result.status === "error") {
|
|
2826
2884
|
failedScenarioIds.add(scenario.id);
|
|
@@ -2867,6 +2925,10 @@ async function runBatch(scenarios, options) {
|
|
|
2867
2925
|
emit({ type: "run:complete", runId: run.id });
|
|
2868
2926
|
const eventType = finalRun.status === "failed" ? "failed" : "completed";
|
|
2869
2927
|
dispatchWebhooks(eventType, finalRun).catch(() => {});
|
|
2928
|
+
if (finalRun.status === "failed") {
|
|
2929
|
+
const failedResults = results.filter((r) => r.status === "failed" || r.status === "error");
|
|
2930
|
+
pushFailedRunToLogs(finalRun, failedResults, scenarios).catch(() => {});
|
|
2931
|
+
}
|
|
2870
2932
|
return { run: finalRun, results };
|
|
2871
2933
|
}
|
|
2872
2934
|
async function runByFilter(options) {
|
|
@@ -3468,6 +3530,10 @@ var chalkStderr = createChalk({ level: stderrColor ? stderrColor.level : 0 });
|
|
|
3468
3530
|
var source_default = chalk;
|
|
3469
3531
|
|
|
3470
3532
|
// src/lib/reporter.ts
|
|
3533
|
+
init_database();
|
|
3534
|
+
function useEmoji() {
|
|
3535
|
+
return !process.env["NO_COLOR"] && process.argv.indexOf("--no-color") === -1;
|
|
3536
|
+
}
|
|
3471
3537
|
function formatTerminal(run, results) {
|
|
3472
3538
|
const lines = [];
|
|
3473
3539
|
lines.push("");
|
|
@@ -3482,21 +3548,22 @@ function formatTerminal(run, results) {
|
|
|
3482
3548
|
const screenshotCount = screenshots.length;
|
|
3483
3549
|
let statusIcon;
|
|
3484
3550
|
let statusColor;
|
|
3551
|
+
const emoji = useEmoji();
|
|
3485
3552
|
switch (result.status) {
|
|
3486
3553
|
case "passed":
|
|
3487
|
-
statusIcon = source_default.green("PASS");
|
|
3554
|
+
statusIcon = emoji ? "\u2705" : source_default.green("PASS");
|
|
3488
3555
|
statusColor = source_default.green;
|
|
3489
3556
|
break;
|
|
3490
3557
|
case "failed":
|
|
3491
|
-
statusIcon = source_default.red("FAIL");
|
|
3558
|
+
statusIcon = emoji ? "\u274C" : source_default.red("FAIL");
|
|
3492
3559
|
statusColor = source_default.red;
|
|
3493
3560
|
break;
|
|
3494
3561
|
case "error":
|
|
3495
|
-
statusIcon = source_default.yellow("ERR ");
|
|
3562
|
+
statusIcon = emoji ? "\u26A0\uFE0F " : source_default.yellow("ERR ");
|
|
3496
3563
|
statusColor = source_default.yellow;
|
|
3497
3564
|
break;
|
|
3498
3565
|
default:
|
|
3499
|
-
statusIcon = source_default.dim("SKIP");
|
|
3566
|
+
statusIcon = emoji ? "\u23ED\uFE0F " : source_default.dim("SKIP");
|
|
3500
3567
|
statusColor = source_default.dim;
|
|
3501
3568
|
break;
|
|
3502
3569
|
}
|
|
@@ -3509,7 +3576,7 @@ function formatTerminal(run, results) {
|
|
|
3509
3576
|
}
|
|
3510
3577
|
}
|
|
3511
3578
|
lines.push("");
|
|
3512
|
-
lines.push(
|
|
3579
|
+
lines.push(formatActionableSummary(run, results));
|
|
3513
3580
|
lines.push("");
|
|
3514
3581
|
return lines.join(`
|
|
3515
3582
|
`);
|
|
@@ -3521,6 +3588,30 @@ function formatSummary(run) {
|
|
|
3521
3588
|
const totalStr = source_default.dim(` (${run.total} total)`);
|
|
3522
3589
|
return ` ${passedStr}${failedStr}${totalStr} in ${duration}`;
|
|
3523
3590
|
}
|
|
3591
|
+
function formatActionableSummary(run, results) {
|
|
3592
|
+
const emoji = useEmoji();
|
|
3593
|
+
const passedCount = results.filter((r) => r.status === "passed").length;
|
|
3594
|
+
const failedCount = results.filter((r) => r.status === "failed" || r.status === "error").length;
|
|
3595
|
+
const shortId = run.id.slice(0, 8);
|
|
3596
|
+
const passStr = `${emoji ? "\u2705" : "PASS"} ${passedCount} passed`;
|
|
3597
|
+
const failStr = failedCount > 0 ? ` ${emoji ? "\u274C" : "FAIL"} ${failedCount} failed` : "";
|
|
3598
|
+
const lines = [];
|
|
3599
|
+
lines.push(` ${source_default.bold(passStr)}${failedCount > 0 ? source_default.bold(failStr) : ""}`);
|
|
3600
|
+
if (failedCount > 0) {
|
|
3601
|
+
lines.push(source_default.dim(` retry failed: testers retry ${shortId} | view: testers results ${shortId}`));
|
|
3602
|
+
} else {
|
|
3603
|
+
lines.push(source_default.dim(` view: testers results ${shortId}`));
|
|
3604
|
+
}
|
|
3605
|
+
const totalCostCents = results.reduce((sum, r) => sum + (r.costCents ?? 0), 0);
|
|
3606
|
+
const totalTokens = results.reduce((sum, r) => sum + (r.tokensUsed ?? 0), 0);
|
|
3607
|
+
if (totalTokens > 0) {
|
|
3608
|
+
const costStr = `$${(totalCostCents / 100).toFixed(4)}`;
|
|
3609
|
+
const tokensStr = totalTokens.toLocaleString();
|
|
3610
|
+
lines.push(source_default.dim(` ${emoji ? "\uD83D\uDCB0" : "cost:"} Cost: ${costStr} (${tokensStr} tokens)`));
|
|
3611
|
+
}
|
|
3612
|
+
return lines.join(`
|
|
3613
|
+
`);
|
|
3614
|
+
}
|
|
3524
3615
|
function formatJSON(run, results) {
|
|
3525
3616
|
const output = {
|
|
3526
3617
|
run: {
|
|
@@ -3599,6 +3690,15 @@ function formatRunList(runs) {
|
|
|
3599
3690
|
return lines.join(`
|
|
3600
3691
|
`);
|
|
3601
3692
|
}
|
|
3693
|
+
function getScenarioRunStats(scenarioId) {
|
|
3694
|
+
const db2 = getDatabase();
|
|
3695
|
+
const lastRow = db2.query("SELECT status FROM results WHERE scenario_id = ? ORDER BY created_at DESC LIMIT 1").get(scenarioId);
|
|
3696
|
+
const statsRow = db2.query("SELECT COUNT(*) as total, SUM(CASE WHEN status = 'passed' THEN 1 ELSE 0 END) as passed FROM results WHERE scenario_id = ?").get(scenarioId);
|
|
3697
|
+
return {
|
|
3698
|
+
lastStatus: lastRow ? lastRow.status : null,
|
|
3699
|
+
passRate: statsRow && statsRow.total > 0 ? `${statsRow.passed}/${statsRow.total}` : "\u2014"
|
|
3700
|
+
};
|
|
3701
|
+
}
|
|
3602
3702
|
function formatScenarioList(scenarios) {
|
|
3603
3703
|
const lines = [];
|
|
3604
3704
|
lines.push("");
|
|
@@ -3613,7 +3713,21 @@ function formatScenarioList(scenarios) {
|
|
|
3613
3713
|
for (const s of scenarios) {
|
|
3614
3714
|
const priorityColor = s.priority === "critical" ? source_default.red : s.priority === "high" ? source_default.yellow : s.priority === "medium" ? source_default.blue : source_default.dim;
|
|
3615
3715
|
const tags = s.tags.length > 0 ? source_default.dim(` [${s.tags.join(", ")}]`) : "";
|
|
3616
|
-
|
|
3716
|
+
let lastStatusIcon = source_default.dim("\u2014");
|
|
3717
|
+
let passRateStr = source_default.dim("\u2014");
|
|
3718
|
+
if (s.id) {
|
|
3719
|
+
const stats = getScenarioRunStats(s.id);
|
|
3720
|
+
if (stats.lastStatus === "passed")
|
|
3721
|
+
lastStatusIcon = source_default.green("\u2713");
|
|
3722
|
+
else if (stats.lastStatus === "failed")
|
|
3723
|
+
lastStatusIcon = source_default.red("\u2717");
|
|
3724
|
+
else if (stats.lastStatus === "error")
|
|
3725
|
+
lastStatusIcon = source_default.yellow("!");
|
|
3726
|
+
else if (stats.lastStatus === "skipped")
|
|
3727
|
+
lastStatusIcon = source_default.dim("~");
|
|
3728
|
+
passRateStr = stats.passRate === "\u2014" ? source_default.dim("\u2014") : source_default.dim(stats.passRate);
|
|
3729
|
+
}
|
|
3730
|
+
lines.push(` ${source_default.cyan(s.shortId)} ${s.name} ${priorityColor(s.priority)}${tags} ${lastStatusIcon} ${passRateStr}`);
|
|
3617
3731
|
}
|
|
3618
3732
|
lines.push("");
|
|
3619
3733
|
return lines.join(`
|
|
@@ -4029,26 +4143,146 @@ function detectFramework(dir) {
|
|
|
4029
4143
|
return null;
|
|
4030
4144
|
}
|
|
4031
4145
|
function getStarterScenarios(framework, projectId) {
|
|
4146
|
+
if (framework.name === "Next.js") {
|
|
4147
|
+
const scenarios2 = [
|
|
4148
|
+
{
|
|
4149
|
+
name: "Homepage loads",
|
|
4150
|
+
description: "Navigate to the homepage and verify it loads correctly. Check that the main heading and content are visible, and there are no console errors.",
|
|
4151
|
+
tags: ["smoke"],
|
|
4152
|
+
priority: "high",
|
|
4153
|
+
projectId
|
|
4154
|
+
},
|
|
4155
|
+
{
|
|
4156
|
+
name: "404 page works",
|
|
4157
|
+
description: "Navigate to a non-existent URL (e.g. /this-page-does-not-exist) and verify the Next.js 404 page renders correctly.",
|
|
4158
|
+
tags: ["smoke"],
|
|
4159
|
+
priority: "medium",
|
|
4160
|
+
projectId
|
|
4161
|
+
},
|
|
4162
|
+
{
|
|
4163
|
+
name: "Navigation links work",
|
|
4164
|
+
description: "Click through the main navigation links and verify each page loads without errors. Check that client-side routing is working correctly.",
|
|
4165
|
+
tags: ["smoke"],
|
|
4166
|
+
priority: "medium",
|
|
4167
|
+
projectId
|
|
4168
|
+
}
|
|
4169
|
+
];
|
|
4170
|
+
if (framework.features.includes("hasAuth")) {
|
|
4171
|
+
scenarios2.push({
|
|
4172
|
+
name: "Login flow",
|
|
4173
|
+
description: "Navigate to the login page, enter valid credentials, and verify successful authentication and redirect.",
|
|
4174
|
+
tags: ["auth"],
|
|
4175
|
+
priority: "critical",
|
|
4176
|
+
projectId
|
|
4177
|
+
}, {
|
|
4178
|
+
name: "Protected route redirect",
|
|
4179
|
+
description: "Try to access a protected route without authentication and verify you are redirected to the login page.",
|
|
4180
|
+
tags: ["auth"],
|
|
4181
|
+
priority: "high",
|
|
4182
|
+
projectId
|
|
4183
|
+
});
|
|
4184
|
+
}
|
|
4185
|
+
if (framework.features.includes("hasForms")) {
|
|
4186
|
+
scenarios2.push({
|
|
4187
|
+
name: "Form validation",
|
|
4188
|
+
description: "Submit forms with empty/invalid data and verify validation errors appear correctly.",
|
|
4189
|
+
tags: ["forms"],
|
|
4190
|
+
priority: "medium",
|
|
4191
|
+
projectId
|
|
4192
|
+
});
|
|
4193
|
+
}
|
|
4194
|
+
return scenarios2;
|
|
4195
|
+
}
|
|
4196
|
+
if (framework.name === "Vite" || framework.name === "SvelteKit") {
|
|
4197
|
+
const scenarios2 = [
|
|
4198
|
+
{
|
|
4199
|
+
name: "Homepage loads",
|
|
4200
|
+
description: "Navigate to the homepage and verify it loads correctly with no console errors.",
|
|
4201
|
+
tags: ["smoke"],
|
|
4202
|
+
priority: "high",
|
|
4203
|
+
projectId
|
|
4204
|
+
},
|
|
4205
|
+
{
|
|
4206
|
+
name: "Mobile viewport check",
|
|
4207
|
+
description: "Set the viewport to 375x812 (iPhone) and verify the homepage renders correctly without horizontal scrolling or layout issues.",
|
|
4208
|
+
tags: ["responsive"],
|
|
4209
|
+
priority: "medium",
|
|
4210
|
+
projectId
|
|
4211
|
+
},
|
|
4212
|
+
{
|
|
4213
|
+
name: "No console errors",
|
|
4214
|
+
description: "Navigate through the app and verify there are no JavaScript errors or warnings in the browser console.",
|
|
4215
|
+
tags: ["smoke"],
|
|
4216
|
+
priority: "high",
|
|
4217
|
+
projectId
|
|
4218
|
+
}
|
|
4219
|
+
];
|
|
4220
|
+
if (framework.features.includes("hasAuth")) {
|
|
4221
|
+
scenarios2.push({
|
|
4222
|
+
name: "Login flow",
|
|
4223
|
+
description: "Navigate to the login page, enter valid credentials, and verify successful authentication.",
|
|
4224
|
+
tags: ["auth"],
|
|
4225
|
+
priority: "critical",
|
|
4226
|
+
projectId
|
|
4227
|
+
});
|
|
4228
|
+
}
|
|
4229
|
+
return scenarios2;
|
|
4230
|
+
}
|
|
4231
|
+
if (framework.name === "Nuxt") {
|
|
4232
|
+
const scenarios2 = [
|
|
4233
|
+
{
|
|
4234
|
+
name: "Homepage loads",
|
|
4235
|
+
description: "Navigate to the homepage and verify it loads correctly. Check that the main heading and content are visible.",
|
|
4236
|
+
tags: ["smoke"],
|
|
4237
|
+
priority: "high",
|
|
4238
|
+
projectId
|
|
4239
|
+
},
|
|
4240
|
+
{
|
|
4241
|
+
name: "Navigation works",
|
|
4242
|
+
description: "Click through main navigation links and verify each page loads without errors.",
|
|
4243
|
+
tags: ["smoke"],
|
|
4244
|
+
priority: "medium",
|
|
4245
|
+
projectId
|
|
4246
|
+
},
|
|
4247
|
+
{
|
|
4248
|
+
name: "Mobile viewport check",
|
|
4249
|
+
description: "Set the viewport to 375x812 and verify the homepage renders correctly on mobile.",
|
|
4250
|
+
tags: ["responsive"],
|
|
4251
|
+
priority: "medium",
|
|
4252
|
+
projectId
|
|
4253
|
+
}
|
|
4254
|
+
];
|
|
4255
|
+
if (framework.features.includes("hasAuth")) {
|
|
4256
|
+
scenarios2.push({
|
|
4257
|
+
name: "Login flow",
|
|
4258
|
+
description: "Navigate to the login page, enter valid credentials, and verify successful authentication.",
|
|
4259
|
+
tags: ["auth"],
|
|
4260
|
+
priority: "critical",
|
|
4261
|
+
projectId
|
|
4262
|
+
});
|
|
4263
|
+
}
|
|
4264
|
+
return scenarios2;
|
|
4265
|
+
}
|
|
4032
4266
|
const scenarios = [
|
|
4033
4267
|
{
|
|
4034
|
-
name: "
|
|
4035
|
-
description: "Navigate to the
|
|
4268
|
+
name: "Homepage loads",
|
|
4269
|
+
description: "Navigate to the homepage and verify it loads correctly with no console errors. Check that the main heading, navigation, and primary CTA are visible.",
|
|
4036
4270
|
tags: ["smoke"],
|
|
4037
4271
|
priority: "high",
|
|
4038
4272
|
projectId
|
|
4039
4273
|
},
|
|
4040
4274
|
{
|
|
4041
|
-
name: "
|
|
4042
|
-
description: "
|
|
4275
|
+
name: "Form submit works",
|
|
4276
|
+
description: "Find the main form on the page, fill it in with valid test data, submit it, and verify the success state.",
|
|
4043
4277
|
tags: ["smoke"],
|
|
4044
4278
|
priority: "medium",
|
|
4045
4279
|
projectId
|
|
4046
4280
|
},
|
|
4047
4281
|
{
|
|
4048
|
-
name: "
|
|
4049
|
-
description: "
|
|
4050
|
-
tags: ["
|
|
4051
|
-
priority: "
|
|
4282
|
+
name: "Mobile viewport check",
|
|
4283
|
+
description: "Set the viewport to 375x812 (iPhone) and verify the homepage renders correctly without horizontal scrolling or layout issues.",
|
|
4284
|
+
tags: ["responsive"],
|
|
4285
|
+
priority: "medium",
|
|
4052
4286
|
projectId
|
|
4053
4287
|
}
|
|
4054
4288
|
];
|
|
@@ -4891,12 +5125,13 @@ function formatCostsTerminal(summary) {
|
|
|
4891
5125
|
}
|
|
4892
5126
|
if (summary.byScenario.length > 0) {
|
|
4893
5127
|
lines.push("");
|
|
4894
|
-
lines.push(source_default.bold("
|
|
4895
|
-
lines.push(` ${"Scenario".padEnd(40)} ${"Cost".padEnd(12)} ${"
|
|
4896
|
-
lines.push(` ${"\u2500".repeat(40)} ${"\u2500".repeat(12)} ${"\u2500".repeat(12)} ${"\u2500".repeat(6)}`);
|
|
5128
|
+
lines.push(source_default.bold(" Scenarios by Cost (most expensive first)"));
|
|
5129
|
+
lines.push(` ${"Scenario".padEnd(40)} ${"Total Cost".padEnd(12)} ${"Avg/Run".padEnd(12)} ${"Runs".padEnd(6)} Tokens`);
|
|
5130
|
+
lines.push(` ${"\u2500".repeat(40)} ${"\u2500".repeat(12)} ${"\u2500".repeat(12)} ${"\u2500".repeat(6)} ${"\u2500".repeat(10)}`);
|
|
4897
5131
|
for (const s of summary.byScenario) {
|
|
4898
5132
|
const label = s.name.length > 38 ? s.name.slice(0, 35) + "..." : s.name;
|
|
4899
|
-
|
|
5133
|
+
const avgPerRun = s.runs > 0 ? s.costCents / s.runs : 0;
|
|
5134
|
+
lines.push(` ${label.padEnd(40)} ${formatDollars(s.costCents).padEnd(12)} ${formatDollars(avgPerRun).padEnd(12)} ${String(s.runs).padEnd(6)} ${formatTokens(s.tokens)}`);
|
|
4900
5135
|
}
|
|
4901
5136
|
}
|
|
4902
5137
|
lines.push("");
|
package/dist/lib/costs.d.ts
CHANGED
|
@@ -33,4 +33,5 @@ export declare function checkBudget(estimatedCostCents: number): {
|
|
|
33
33
|
};
|
|
34
34
|
export declare function formatCostsTerminal(summary: CostSummary): string;
|
|
35
35
|
export declare function formatCostsJSON(summary: CostSummary): string;
|
|
36
|
+
export declare function formatCostsCsv(summary: CostSummary): string;
|
|
36
37
|
//# sourceMappingURL=costs.d.ts.map
|
package/dist/lib/costs.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"costs.d.ts","sourceRoot":"","sources":["../../src/lib/costs.ts"],"names":[],"mappings":"AAMA,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,MAAM,CAAC;IACf,cAAc,EAAE,MAAM,CAAC;IACvB,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC7E,UAAU,EAAE,KAAK,CAAC;QAAE,UAAU,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACzG,aAAa,EAAE,MAAM,CAAC;IACtB,qBAAqB,EAAE,MAAM,CAAC;CAC/B;AAED,MAAM,WAAW,YAAY;IAC3B,cAAc,EAAE,MAAM,CAAC;IACvB,cAAc,EAAE,MAAM,CAAC;IACvB,aAAa,EAAE,MAAM,CAAC;CACvB;AA0CD,wBAAgB,cAAc,CAAC,OAAO,CAAC,EAAE;IACvC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,KAAK,GAAG,MAAM,GAAG,OAAO,GAAG,KAAK,CAAC;CAC3C,GAAG,WAAW,CAyFd;AAED,wBAAgB,WAAW,CAAC,kBAAkB,EAAE,MAAM,GAAG;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAAE,CA+B9F;AAcD,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,WAAW,GAAG,MAAM,
|
|
1
|
+
{"version":3,"file":"costs.d.ts","sourceRoot":"","sources":["../../src/lib/costs.ts"],"names":[],"mappings":"AAMA,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,MAAM,CAAC;IACf,cAAc,EAAE,MAAM,CAAC;IACvB,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC7E,UAAU,EAAE,KAAK,CAAC;QAAE,UAAU,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACzG,aAAa,EAAE,MAAM,CAAC;IACtB,qBAAqB,EAAE,MAAM,CAAC;CAC/B;AAED,MAAM,WAAW,YAAY;IAC3B,cAAc,EAAE,MAAM,CAAC;IACvB,cAAc,EAAE,MAAM,CAAC;IACvB,aAAa,EAAE,MAAM,CAAC;CACvB;AA0CD,wBAAgB,cAAc,CAAC,OAAO,CAAC,EAAE;IACvC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,KAAK,GAAG,MAAM,GAAG,OAAO,GAAG,KAAK,CAAC;CAC3C,GAAG,WAAW,CAyFd;AAED,wBAAgB,WAAW,CAAC,kBAAkB,EAAE,MAAM,GAAG;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAAE,CA+B9F;AAcD,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,WAAW,GAAG,MAAM,CAyChE;AAED,wBAAgB,eAAe,CAAC,OAAO,EAAE,WAAW,GAAG,MAAM,CAE5D;AAED,wBAAgB,cAAc,CAAC,OAAO,EAAE,WAAW,GAAG,MAAM,CAS3D"}
|
package/dist/lib/init.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../src/lib/init.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AACpD,OAAO,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAClD,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AAI7D,UAAU,aAAa;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB;AAED,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,aAAa,GAAG,IAAI,CAmDjE;AAID,wBAAgB,mBAAmB,CACjC,SAAS,EAAE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,EAAE,CAAA;CAAE,EAC/C,SAAS,EAAE,MAAM,GAChB,mBAAmB,EAAE,
|
|
1
|
+
{"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../src/lib/init.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AACpD,OAAO,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAClD,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AAI7D,UAAU,aAAa;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB;AAED,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,aAAa,GAAG,IAAI,CAmDjE;AAID,wBAAgB,mBAAmB,CACjC,SAAS,EAAE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,EAAE,CAAA;CAAE,EAC/C,SAAS,EAAE,MAAM,GAChB,mBAAmB,EAAE,CAkNvB;AAID,MAAM,WAAW,WAAW;IAC1B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,UAAU;IACzB,OAAO,EAAE,UAAU,CAAC,OAAO,aAAa,CAAC,CAAC;IAC1C,SAAS,EAAE,UAAU,CAAC,OAAO,cAAc,CAAC,EAAE,CAAC;IAC/C,SAAS,EAAE,aAAa,GAAG,IAAI,CAAC;IAChC,GAAG,EAAE,MAAM,CAAC;CACb;AAED,wBAAgB,WAAW,CAAC,OAAO,EAAE,WAAW,GAAG,UAAU,CAuC5D"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { Run, Result, Scenario } from "../types/index.js";
|
|
2
|
+
/**
|
|
3
|
+
* Push failed test results to open-logs if LOGS_URL env var is set.
|
|
4
|
+
* No-op when LOGS_URL is not configured.
|
|
5
|
+
*/
|
|
6
|
+
export declare function pushFailedRunToLogs(run: Run, failedResults: Result[], scenarios: Scenario[]): Promise<void>;
|
|
7
|
+
//# sourceMappingURL=logs-integration.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"logs-integration.d.ts","sourceRoot":"","sources":["../../src/lib/logs-integration.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAE/D;;;GAGG;AACH,wBAAsB,mBAAmB,CACvC,GAAG,EAAE,GAAG,EACR,aAAa,EAAE,MAAM,EAAE,EACvB,SAAS,EAAE,QAAQ,EAAE,GACpB,OAAO,CAAC,IAAI,CAAC,CAiCf"}
|
package/dist/lib/reporter.d.ts
CHANGED
|
@@ -5,10 +5,17 @@ export interface ReportOptions {
|
|
|
5
5
|
}
|
|
6
6
|
export declare function formatTerminal(run: Run, results: Result[]): string;
|
|
7
7
|
export declare function formatSummary(run: Run): string;
|
|
8
|
+
export declare function formatActionableSummary(run: Run, results: Result[]): string;
|
|
8
9
|
export declare function formatJSON(run: Run, results: Result[]): string;
|
|
9
10
|
export declare function getExitCode(run: Run): number;
|
|
10
11
|
export declare function formatRunList(runs: Run[]): string;
|
|
12
|
+
export interface ScenarioRunStats {
|
|
13
|
+
lastStatus: "passed" | "failed" | "error" | "skipped" | null;
|
|
14
|
+
passRate: string;
|
|
15
|
+
}
|
|
16
|
+
export declare function getScenarioRunStats(scenarioId: string): ScenarioRunStats;
|
|
11
17
|
export declare function formatScenarioList(scenarios: Array<{
|
|
18
|
+
id?: string;
|
|
12
19
|
shortId: string;
|
|
13
20
|
name: string;
|
|
14
21
|
priority: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"reporter.d.ts","sourceRoot":"","sources":["../../src/lib/reporter.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;
|
|
1
|
+
{"version":3,"file":"reporter.d.ts","sourceRoot":"","sources":["../../src/lib/reporter.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAWjE,MAAM,WAAW,aAAa;IAC5B,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,wBAAgB,cAAc,CAAC,GAAG,EAAE,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,MAAM,CAoDlE;AAED,wBAAgB,aAAa,CAAC,GAAG,EAAE,GAAG,GAAG,MAAM,CAU9C;AAED,wBAAgB,uBAAuB,CAAC,GAAG,EAAE,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,MAAM,CA4B3E;AAED,wBAAgB,UAAU,CAAC,GAAG,EAAE,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,MAAM,CAoD9D;AAED,wBAAgB,WAAW,CAAC,GAAG,EAAE,GAAG,GAAG,MAAM,CAI5C;AAED,wBAAgB,aAAa,CAAC,IAAI,EAAE,GAAG,EAAE,GAAG,MAAM,CA6BjD;AAED,MAAM,WAAW,gBAAgB;IAC/B,UAAU,EAAE,QAAQ,GAAG,QAAQ,GAAG,OAAO,GAAG,SAAS,GAAG,IAAI,CAAC;IAC7D,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,wBAAgB,mBAAmB,CAAC,UAAU,EAAE,MAAM,GAAG,gBAAgB,CAiBxE;AAED,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,KAAK,CAAC;IAAE,EAAE,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,EAAE,CAAA;CAAE,CAAC,GAAG,MAAM,CAwC7I;AAED,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,MAAM,EAAE,WAAW,EAAE,UAAU,EAAE,GAAG,MAAM,CAqCpF"}
|
package/dist/lib/runner.d.ts
CHANGED
|
@@ -5,6 +5,7 @@ export interface RunOptions {
|
|
|
5
5
|
headed?: boolean;
|
|
6
6
|
parallel?: number;
|
|
7
7
|
timeout?: number;
|
|
8
|
+
retry?: number;
|
|
8
9
|
projectId?: string;
|
|
9
10
|
apiKey?: string;
|
|
10
11
|
screenshotDir?: string;
|
|
@@ -23,6 +24,8 @@ export interface RunEvent {
|
|
|
23
24
|
toolResult?: string;
|
|
24
25
|
thinking?: string;
|
|
25
26
|
stepNumber?: number;
|
|
27
|
+
retryAttempt?: number;
|
|
28
|
+
maxRetries?: number;
|
|
26
29
|
}
|
|
27
30
|
export type RunEventHandler = (event: RunEvent) => void;
|
|
28
31
|
export declare function onRunEvent(handler: RunEventHandler): void;
|
package/dist/lib/runner.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"runner.d.ts","sourceRoot":"","sources":["../../src/lib/runner.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;
|
|
1
|
+
{"version":3,"file":"runner.d.ts","sourceRoot":"","sources":["../../src/lib/runner.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAa/D,MAAM,WAAW,UAAU;IACzB,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,MAAM,CAAC,EAAE,YAAY,GAAG,YAAY,CAAC;CACtC;AAED,MAAM,WAAW,QAAQ;IACvB,IAAI,EACA,gBAAgB,GAChB,eAAe,GACf,eAAe,GACf,gBAAgB,GAChB,qBAAqB,GACrB,cAAc,GACd,gBAAgB,GAChB,kBAAkB,GAClB,eAAe,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACpC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,MAAM,eAAe,GAAG,CAAC,KAAK,EAAE,QAAQ,KAAK,IAAI,CAAC;AAIxD,wBAAgB,UAAU,CAAC,OAAO,EAAE,eAAe,GAAG,IAAI,CAEzD;AAkBD,wBAAsB,iBAAiB,CACrC,QAAQ,EAAE,QAAQ,EAClB,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,UAAU,GAClB,OAAO,CAAC,MAAM,CAAC,CAmGjB;AAED,wBAAsB,QAAQ,CAC5B,SAAS,EAAE,QAAQ,EAAE,EACrB,OAAO,EAAE,UAAU,GAClB,OAAO,CAAC;IAAE,GAAG,EAAE,GAAG,CAAC;IAAC,OAAO,EAAE,MAAM,EAAE,CAAA;CAAE,CAAC,CAsI1C;AAED,wBAAsB,WAAW,CAC/B,OAAO,EAAE,UAAU,GAAG;IAAE,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAAC,WAAW,CAAC,EAAE,MAAM,EAAE,CAAA;CAAE,GACnF,OAAO,CAAC;IAAE,GAAG,EAAE,GAAG,CAAC;IAAC,OAAO,EAAE,MAAM,EAAE,CAAA;CAAE,CAAC,CAuB1C;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAC3B,OAAO,EAAE,UAAU,GAAG;IAAE,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAAC,WAAW,CAAC,EAAE,MAAM,EAAE,CAAA;CAAE,GACnF;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,aAAa,EAAE,MAAM,CAAA;CAAE,CAmF1C"}
|
package/dist/mcp/index.js
CHANGED
|
@@ -6272,6 +6272,38 @@ async function dispatchWebhooks(event, run, schedule) {
|
|
|
6272
6272
|
}
|
|
6273
6273
|
}
|
|
6274
6274
|
|
|
6275
|
+
// src/lib/logs-integration.ts
|
|
6276
|
+
async function pushFailedRunToLogs(run, failedResults, scenarios) {
|
|
6277
|
+
const logsUrl = process.env.LOGS_URL;
|
|
6278
|
+
if (!logsUrl)
|
|
6279
|
+
return;
|
|
6280
|
+
const scenarioMap = new Map(scenarios.map((s) => [s.id, s]));
|
|
6281
|
+
const entries = failedResults.map((result) => {
|
|
6282
|
+
const scenario = scenarioMap.get(result.scenarioId);
|
|
6283
|
+
return {
|
|
6284
|
+
level: "error",
|
|
6285
|
+
source: "sdk",
|
|
6286
|
+
service: "testers",
|
|
6287
|
+
message: `[testers] Scenario failed: ${scenario?.name ?? result.scenarioId}${result.error ? ` \u2014 ${result.error}` : ""}`,
|
|
6288
|
+
metadata: {
|
|
6289
|
+
run_id: run.id,
|
|
6290
|
+
scenario_id: result.scenarioId,
|
|
6291
|
+
scenario_name: scenario?.name,
|
|
6292
|
+
url: run.url,
|
|
6293
|
+
status: result.status,
|
|
6294
|
+
duration_ms: result.durationMs
|
|
6295
|
+
}
|
|
6296
|
+
};
|
|
6297
|
+
});
|
|
6298
|
+
try {
|
|
6299
|
+
await fetch(`${logsUrl.replace(/\/$/, "")}/api/logs`, {
|
|
6300
|
+
method: "POST",
|
|
6301
|
+
headers: { "Content-Type": "application/json" },
|
|
6302
|
+
body: JSON.stringify(entries)
|
|
6303
|
+
});
|
|
6304
|
+
} catch {}
|
|
6305
|
+
}
|
|
6306
|
+
|
|
6275
6307
|
// src/lib/runner.ts
|
|
6276
6308
|
var eventHandler = null;
|
|
6277
6309
|
function emit(event) {
|
|
@@ -6281,7 +6313,7 @@ function emit(event) {
|
|
|
6281
6313
|
function withTimeout(promise, ms, label) {
|
|
6282
6314
|
return new Promise((resolve, reject) => {
|
|
6283
6315
|
const timer = setTimeout(() => {
|
|
6284
|
-
reject(new Error(`Scenario
|
|
6316
|
+
reject(new Error(`Scenario '${label}' timed out after ${ms}ms. Try: testers run --timeout ${ms * 2} or simplify the scenario steps.`));
|
|
6285
6317
|
}, ms);
|
|
6286
6318
|
promise.then((val) => {
|
|
6287
6319
|
clearTimeout(timer);
|
|
@@ -6414,6 +6446,7 @@ async function runBatch(scenarios, options) {
|
|
|
6414
6446
|
} catch {}
|
|
6415
6447
|
return true;
|
|
6416
6448
|
};
|
|
6449
|
+
const maxRetries = options.retry ?? 0;
|
|
6417
6450
|
if (parallel <= 1) {
|
|
6418
6451
|
for (const scenario of sortedScenarios) {
|
|
6419
6452
|
if (!await canRun(scenario)) {
|
|
@@ -6424,7 +6457,13 @@ async function runBatch(scenarios, options) {
|
|
|
6424
6457
|
emit({ type: "scenario:error", scenarioId: scenario.id, scenarioName: scenario.name, error: "Dependency failed \u2014 skipped", runId: run.id });
|
|
6425
6458
|
continue;
|
|
6426
6459
|
}
|
|
6427
|
-
|
|
6460
|
+
let result = await runSingleScenario(scenario, run.id, options);
|
|
6461
|
+
let attempt = 1;
|
|
6462
|
+
while ((result.status === "failed" || result.status === "error") && attempt <= maxRetries) {
|
|
6463
|
+
emit({ type: "scenario:start", scenarioId: scenario.id, scenarioName: scenario.name, runId: run.id, retryAttempt: attempt + 1, maxRetries: maxRetries + 1 });
|
|
6464
|
+
result = await runSingleScenario(scenario, run.id, options);
|
|
6465
|
+
attempt++;
|
|
6466
|
+
}
|
|
6428
6467
|
results.push(result);
|
|
6429
6468
|
if (result.status === "failed" || result.status === "error") {
|
|
6430
6469
|
failedScenarioIds.add(scenario.id);
|
|
@@ -6471,6 +6510,10 @@ async function runBatch(scenarios, options) {
|
|
|
6471
6510
|
emit({ type: "run:complete", runId: run.id });
|
|
6472
6511
|
const eventType = finalRun.status === "failed" ? "failed" : "completed";
|
|
6473
6512
|
dispatchWebhooks(eventType, finalRun).catch(() => {});
|
|
6513
|
+
if (finalRun.status === "failed") {
|
|
6514
|
+
const failedResults = results.filter((r) => r.status === "failed" || r.status === "error");
|
|
6515
|
+
pushFailedRunToLogs(finalRun, failedResults, scenarios).catch(() => {});
|
|
6516
|
+
}
|
|
6474
6517
|
return { run: finalRun, results };
|
|
6475
6518
|
}
|
|
6476
6519
|
async function runByFilter(options) {
|