@empiricalrun/test-gen 0.16.3 → 0.16.5

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/CHANGELOG.md CHANGED
@@ -1,5 +1,18 @@
1
1
  # @empiricalrun/test-gen
2
2
 
3
+ ## 0.16.5
4
+
5
+ ### Patch Changes
6
+
7
+ - Updated dependencies [0b41c5e]
8
+ - @empiricalrun/reporter@0.11.1
9
+
10
+ ## 0.16.4
11
+
12
+ ### Patch Changes
13
+
14
+ - 4c32cf9: feat: add support for uploading test video of browsing agent generated tests
15
+
3
16
  ## 0.16.3
4
17
 
5
18
  ### Patch Changes
package/dist/bin/index.js CHANGED
@@ -9,9 +9,9 @@ const dotenv_1 = __importDefault(require("dotenv"));
9
9
  const run_1 = require("../agent/browsing/run");
10
10
  const utils_1 = require("../agent/browsing/utils");
11
11
  const run_2 = require("../agent/codegen/run");
12
+ const reporter_1 = require("../reporter");
13
+ const ci_1 = require("../reporter/ci");
12
14
  const logger_1 = require("./logger");
13
- const reporter_1 = require("./reporter");
14
- const ci_1 = require("./reporter/ci");
15
15
  const utils_2 = require("./utils");
16
16
  dotenv_1.default.config({
17
17
  path: [".env.local", ".env"],
@@ -30,6 +30,9 @@ async function runAgent(sourceFile, isUpdate, testGenConfigs) {
30
30
  logger.success("Generating test using browsing agent");
31
31
  await (0, utils_1.prepareFileForBrowsingAgent)(testGenConfig);
32
32
  await (0, run_1.generateTestsUsingBrowsingAgent)(specPath);
33
+ await (0, reporter_1.reportTestGenVideos)({
34
+ projectRepoName: testGenConfig.options.metadata.projectRepoName,
35
+ });
33
36
  generatedTestScenarios.push(...testGenConfig.scenarios);
34
37
  }
35
38
  else {
@@ -49,6 +52,7 @@ async function runAgent(sourceFile, isUpdate, testGenConfigs) {
49
52
  const { sourceFile, testGenConfigs, isUpdate } = await (0, utils_2.parseCliArgs)();
50
53
  (0, reporter_1.setReporterConfig)(testGenConfigs[0]?.options?.metadata);
51
54
  const generated = await runAgent(sourceFile, isUpdate, testGenConfigs);
55
+ // TODO: move these reporters to a better lifecycle
52
56
  await (0, ci_1.reportOnCI)(generated);
53
57
  process.exit(0);
54
58
  })();
@@ -3,7 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.CustomLogger = void 0;
4
4
  const reporter_1 = require("@empiricalrun/reporter");
5
5
  const picocolors_1 = require("picocolors");
6
- const reporter_2 = require("../reporter");
6
+ const reporter_2 = require("../../reporter");
7
7
  class CustomLogger {
8
8
  useReporter = false;
9
9
  constructor({ useReporter = true } = {}) {
package/dist/index.js CHANGED
@@ -6,9 +6,9 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.createTest = void 0;
7
7
  const llm_1 = require("@empiricalrun/llm");
8
8
  const browsing_1 = require("./agent/browsing");
9
- const reporter_1 = require("./bin/reporter");
10
9
  const utils_1 = require("./bin/utils");
11
10
  const client_1 = __importDefault(require("./file/client"));
11
+ const reporter_1 = require("./reporter");
12
12
  process.on("exit", async () => await (0, llm_1.flushAllTraces)());
13
13
  process.on("SIGINT", async () => await (0, llm_1.flushAllTraces)());
14
14
  process.on("SIGTERM", async () => await (0, llm_1.flushAllTraces)());
@@ -1,3 +1,3 @@
1
- import { Scenario } from "../../types";
1
+ import { Scenario } from "../types";
2
2
  export declare function reportOnCI(scenarios: Scenario[]): Promise<Scenario[]>;
3
3
  //# sourceMappingURL=ci.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ci.d.ts","sourceRoot":"","sources":["../../src/reporter/ci.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,QAAQ,EAAE,MAAM,UAAU,CAAC;AAEpC,wBAAsB,UAAU,CAAC,SAAS,EAAE,QAAQ,EAAE,uBAerD"}
@@ -0,0 +1,17 @@
1
+ import { Reporter } from "@empiricalrun/reporter";
2
+ export declare function getReporter(): Reporter | undefined;
3
+ /**
4
+ * function will upload videos and json summary of test results to r2 and report them to reporter.
5
+ * method won't throw error if it fails to report
6
+ * @param {{
7
+ * projectRepoName: string;
8
+ * }} {
9
+ * projectRepoName
10
+ * }
11
+ * @returns Promise<void> returns void
12
+ */
13
+ export declare function reportTestGenVideos({ projectRepoName, }: {
14
+ projectRepoName: string;
15
+ }): Promise<void>;
16
+ export declare function setReporterConfig(config: any): void;
17
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/reporter/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAA4B,QAAQ,EAAE,MAAM,wBAAwB,CAAC;AAW5E,wBAAgB,WAAW,IAAI,QAAQ,GAAG,SAAS,CAUlD;AAED;;;;;;;;;GASG;AACH,wBAAsB,mBAAmB,CAAC,EACxC,eAAe,GAChB,EAAE;IACD,eAAe,EAAE,MAAM,CAAC;CACzB,iBA4BA;AAED,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,GAAG,GAAG,IAAI,CAGnD"}
@@ -0,0 +1,62 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.setReporterConfig = exports.reportTestGenVideos = exports.getReporter = void 0;
4
+ const reporter_1 = require("@empiricalrun/reporter");
5
+ const logger_1 = require("../bin/logger");
6
+ const uploader_1 = require("../uploader");
7
+ let reporterInstance = undefined;
8
+ let reporterConfig = undefined;
9
+ function getReporter() {
10
+ // if no config is set, return undefined
11
+ if (!reporterConfig) {
12
+ console.warn("initialising reporter without any config");
13
+ }
14
+ // initialise once config is set
15
+ if (!reporterInstance && reporterConfig) {
16
+ reporterInstance = new reporter_1.Reporter(reporterConfig);
17
+ }
18
+ return reporterInstance;
19
+ }
20
+ exports.getReporter = getReporter;
21
+ /**
22
+ * function will upload videos and json summary of test results to r2 and report them to reporter.
23
+ * method won't throw error if it fails to report
24
+ * @param {{
25
+ * projectRepoName: string;
26
+ * }} {
27
+ * projectRepoName
28
+ * }
29
+ * @returns Promise<void> returns void
30
+ */
31
+ async function reportTestGenVideos({ projectRepoName, }) {
32
+ const logger = new logger_1.CustomLogger();
33
+ try {
34
+ if (!(0, uploader_1.checkIfResultsUploadAllowed)()) {
35
+ logger.log("Skipped uploading generated test video");
36
+ }
37
+ const { videoUrls } = await (0, uploader_1.uploadTestResultsUsingPrjtRepo)({
38
+ projectRepoName,
39
+ });
40
+ const reporter = getReporter();
41
+ const reporterMessage = `
42
+
43
+ Here are the videos of the generated test:
44
+
45
+ ${videoUrls
46
+ .map((url) => `
47
+ <video src="${url}" autoplay="true" muted="true" controls loop playsinline></video>`)
48
+ .join("\n")}
49
+ `;
50
+ await reporter?.report(new reporter_1.ProcessLogMessageBuilder({ message: reporterMessage }));
51
+ }
52
+ catch (err) {
53
+ logger.error("Failed to report test results");
54
+ console.error(err);
55
+ }
56
+ }
57
+ exports.reportTestGenVideos = reportTestGenVideos;
58
+ function setReporterConfig(config) {
59
+ console.info("initialised reporter config");
60
+ reporterConfig = config;
61
+ }
62
+ exports.setReporterConfig = setReporterConfig;
@@ -0,0 +1,14 @@
1
+ /**
2
+ * function to upload test results to r2 using the project repo name
3
+ * this only uploads json summary of test results
4
+ * @param { projectName: string } projectRepoName - name of the project repo
5
+ * @returns urls of videos and summary json
6
+ */
7
+ export declare function uploadTestResultsUsingPrjtRepo({ projectRepoName, }: {
8
+ projectRepoName: string;
9
+ }): Promise<{
10
+ videoUrls: string[];
11
+ summaryUrl: string;
12
+ }>;
13
+ export declare function checkIfResultsUploadAllowed(): string | undefined;
14
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/uploader/index.ts"],"names":[],"mappings":"AAiBA;;;;;GAKG;AACH,wBAAsB,8BAA8B,CAAC,EACnD,eAAe,GAChB,EAAE;IACD,eAAe,EAAE,MAAM,CAAC;CACzB,GAAG,OAAO,CAAC;IACV,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;CACpB,CAAC,CAyBD;AAED,wBAAgB,2BAA2B,uBAQ1C"}
@@ -0,0 +1,52 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.checkIfResultsUploadAllowed = exports.uploadTestResultsUsingPrjtRepo = void 0;
7
+ const path_1 = __importDefault(require("path"));
8
+ const r2_1 = require("./r2");
9
+ // json summary of test results
10
+ const TEST_RESULTS_DIR = "test-results";
11
+ const UPLOAD_BUCKET = "test-report";
12
+ const UPLOAD_DOMAIN = "https://reports.empirical.run"; // domain based on bucket mentioned above
13
+ function getFullUploadPath(filePath, uploadDir) {
14
+ const relativeFilePath = filePath.replace(path_1.default.join(process.cwd(), TEST_RESULTS_DIR), "");
15
+ return `${UPLOAD_DOMAIN}/${uploadDir}${relativeFilePath}`;
16
+ }
17
+ /**
18
+ * function to upload test results to r2 using the project repo name
19
+ * this only uploads json summary of test results
20
+ * @param { projectName: string } projectRepoName - name of the project repo
21
+ * @returns urls of videos and summary json
22
+ */
23
+ async function uploadTestResultsUsingPrjtRepo({ projectRepoName, }) {
24
+ const uploadUniqueId = crypto.randomUUID();
25
+ // project repo name is the github repo name
26
+ // the folder names in r2 are the github repo name without the `-tests` suffix
27
+ const uploadDir = `test-generation/${projectRepoName.replace("-tests", "")}/${uploadUniqueId}`;
28
+ const files = await (0, r2_1.uploadDirectory)({
29
+ sourceDir: path_1.default.join(process.cwd(), TEST_RESULTS_DIR),
30
+ destinationDir: uploadDir,
31
+ uploadBucket: UPLOAD_BUCKET,
32
+ });
33
+ const fileNames = Object.keys(files);
34
+ // TODO: parse the json summary and then detect video attachments
35
+ // current assumption
36
+ // - test gen will only run on a single spec file
37
+ // - the video files will be of format <spec-file-name>.webm
38
+ const videoFiles = fileNames.filter((fileName) => fileName.endsWith(".webm"));
39
+ return {
40
+ videoUrls: videoFiles.map((fileName) => getFullUploadPath(fileName, uploadDir)),
41
+ summaryUrl: getFullUploadPath("/test-results/summary.json", uploadDir),
42
+ };
43
+ }
44
+ exports.uploadTestResultsUsingPrjtRepo = uploadTestResultsUsingPrjtRepo;
45
+ function checkIfResultsUploadAllowed() {
46
+ // TODO: check for valid R2 credentials
47
+ // check for project repo name, and r2 creds
48
+ return (process.env.R2_ACCOUNT_ID &&
49
+ process.env.R2_ACCESS_KEY_ID &&
50
+ process.env.R2_SECRET_ACCESS_KEY);
51
+ }
52
+ exports.checkIfResultsUploadAllowed = checkIfResultsUploadAllowed;
@@ -0,0 +1,10 @@
1
+ interface FileMap {
2
+ [file: string]: string;
3
+ }
4
+ export declare function uploadDirectory({ sourceDir, destinationDir, uploadBucket, }: {
5
+ sourceDir: string;
6
+ destinationDir: string;
7
+ uploadBucket: string;
8
+ }): Promise<FileMap>;
9
+ export {};
10
+ //# sourceMappingURL=r2.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"r2.d.ts","sourceRoot":"","sources":["../../src/uploader/r2.ts"],"names":[],"mappings":"AAqBA,UAAU,OAAO;IACf,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC;CACxB;AAgGD,wBAAsB,eAAe,CAAC,EACpC,SAAS,EACT,cAAc,EACd,YAAY,GACb,EAAE;IACD,SAAS,EAAE,MAAM,CAAC;IAClB,cAAc,EAAE,MAAM,CAAC;IACvB,YAAY,EAAE,MAAM,CAAC;CACtB,GAAG,OAAO,CAAC,OAAO,CAAC,CAWnB"}
@@ -0,0 +1,124 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || function (mod) {
19
+ if (mod && mod.__esModule) return mod;
20
+ var result = {};
21
+ if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
22
+ __setModuleDefault(result, mod);
23
+ return result;
24
+ };
25
+ var __importDefault = (this && this.__importDefault) || function (mod) {
26
+ return (mod && mod.__esModule) ? mod : { "default": mod };
27
+ };
28
+ Object.defineProperty(exports, "__esModule", { value: true });
29
+ exports.uploadDirectory = void 0;
30
+ const client_s3_1 = require("@aws-sdk/client-s3");
31
+ const s3_request_presigner_1 = require("@aws-sdk/s3-request-presigner");
32
+ const fs = __importStar(require("fs"));
33
+ const md5_1 = __importDefault(require("md5"));
34
+ const path_1 = __importDefault(require("path"));
35
+ const getFileList = (dir) => {
36
+ let files = [];
37
+ const items = fs.readdirSync(dir, {
38
+ withFileTypes: true,
39
+ });
40
+ for (const item of items) {
41
+ const isDir = item.isDirectory();
42
+ const absolutePath = `${dir}/${item.name}`;
43
+ if (isDir) {
44
+ files = [...files, ...getFileList(absolutePath)];
45
+ }
46
+ else {
47
+ files.push(absolutePath);
48
+ }
49
+ }
50
+ return files;
51
+ };
52
+ const run = async (config) => {
53
+ const map = new Map();
54
+ const urls = {};
55
+ const S3 = new client_s3_1.S3Client({
56
+ region: "auto",
57
+ endpoint: `https://${config.accountId}.r2.cloudflarestorage.com`,
58
+ credentials: {
59
+ accessKeyId: config.accessKeyId,
60
+ secretAccessKey: config.secretAccessKey,
61
+ },
62
+ });
63
+ const files = getFileList(config.sourceDir);
64
+ const mime = (await import("mime")).default;
65
+ await Promise.all(files.map(async (file) => {
66
+ console.log(file);
67
+ const fileStream = fs.readFileSync(file);
68
+ console.log(config.sourceDir);
69
+ console.log(config.destinationDir);
70
+ //const fileName = file.replace(/^.*[\\\/]/, "");
71
+ const fileName = file.replace(config.sourceDir, "");
72
+ const fileKey = path_1.default.join(config.destinationDir !== "" ? config.destinationDir : config.sourceDir, fileName);
73
+ if (fileKey.includes(".gitkeep"))
74
+ return;
75
+ console.log(fileKey);
76
+ const mimeType = mime.getType(file);
77
+ const uploadParams = {
78
+ Bucket: config.bucket,
79
+ Key: fileKey,
80
+ Body: fileStream,
81
+ ContentLength: fs.statSync(file).size,
82
+ ContentType: mimeType ?? "application/octet-stream",
83
+ };
84
+ const cmd = new client_s3_1.PutObjectCommand(uploadParams);
85
+ const digest = (0, md5_1.default)(fileStream);
86
+ cmd.middlewareStack.add((next) => async (args) => {
87
+ args.request.headers["if-none-match"] = `"${digest}"`;
88
+ return await next(args);
89
+ }, {
90
+ step: "build",
91
+ name: "addETag",
92
+ });
93
+ try {
94
+ const data = await S3.send(cmd);
95
+ console.log(`R2 Success - ${file}`);
96
+ map.set(file, data);
97
+ const fileUrl = await (0, s3_request_presigner_1.getSignedUrl)(S3, cmd);
98
+ urls[file] = fileUrl;
99
+ }
100
+ catch (err) {
101
+ const error = err;
102
+ if (error["$metadata"]) {
103
+ if (error.$metadata.httpStatusCode !== 412)
104
+ // If-None-Match
105
+ throw error;
106
+ }
107
+ }
108
+ return;
109
+ }));
110
+ return urls;
111
+ };
112
+ async function uploadDirectory({ sourceDir, destinationDir, uploadBucket, }) {
113
+ let config = {
114
+ accountId: process.env.R2_ACCOUNT_ID,
115
+ accessKeyId: process.env.R2_ACCESS_KEY_ID,
116
+ secretAccessKey: process.env.R2_SECRET_ACCESS_KEY,
117
+ bucket: uploadBucket,
118
+ sourceDir,
119
+ destinationDir,
120
+ };
121
+ const uploadedFiles = await run(config);
122
+ return uploadedFiles;
123
+ }
124
+ exports.uploadDirectory = uploadDirectory;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@empiricalrun/test-gen",
3
- "version": "0.16.3",
3
+ "version": "0.16.5",
4
4
  "publishConfig": {
5
5
  "registry": "https://registry.npmjs.org/",
6
6
  "access": "public"
@@ -15,6 +15,8 @@
15
15
  },
16
16
  "author": "Empirical Team <hey@empirical.run>",
17
17
  "dependencies": {
18
+ "@aws-sdk/client-s3": "^3.614.0",
19
+ "@aws-sdk/s3-request-presigner": "^3.614.0",
18
20
  "@types/sanitize-html": "^2.11.0",
19
21
  "commander": "^12.1.0",
20
22
  "detect-port": "^1.6.1",
@@ -25,6 +27,8 @@
25
27
  "google-auth-library": "^9.10.0",
26
28
  "google-spreadsheet": "^4.1.2",
27
29
  "ignore": "^5.3.1",
30
+ "md5": "^2.3.0",
31
+ "mime": "^4.0.4",
28
32
  "minimatch": "^10.0.1",
29
33
  "openai": "^4.47.2",
30
34
  "picocolors": "^1.0.1",
@@ -35,12 +39,13 @@
35
39
  "tsx": "^4.16.2",
36
40
  "typescript": "^5.3.3",
37
41
  "@empiricalrun/llm": "^0.2.0",
38
- "@empiricalrun/reporter": "^0.11.0"
42
+ "@empiricalrun/reporter": "^0.11.1"
39
43
  },
40
44
  "devDependencies": {
41
45
  "@types/detect-port": "^1.3.5",
42
46
  "@types/express": "^4.17.21",
43
- "@types/fs-extra": "^11.0.4"
47
+ "@types/fs-extra": "^11.0.4",
48
+ "@types/md5": "^2.3.5"
44
49
  },
45
50
  "scripts": {
46
51
  "dev": "tsc --build --watch",
@@ -1 +0,0 @@
1
- {"version":3,"file":"ci.d.ts","sourceRoot":"","sources":["../../../src/bin/reporter/ci.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAEvC,wBAAsB,UAAU,CAAC,SAAS,EAAE,QAAQ,EAAE,uBAerD"}
@@ -1,4 +0,0 @@
1
- import { Reporter } from "@empiricalrun/reporter";
2
- export declare function getReporter(): Reporter | undefined;
3
- export declare function setReporterConfig(config: any): void;
4
- //# sourceMappingURL=index.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/bin/reporter/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,wBAAwB,CAAC;AAIlD,wBAAgB,WAAW,IAAI,QAAQ,GAAG,SAAS,CAUlD;AAED,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,GAAG,GAAG,IAAI,CAGnD"}
@@ -1,23 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.setReporterConfig = exports.getReporter = void 0;
4
- const reporter_1 = require("@empiricalrun/reporter");
5
- let reporterInstance = undefined;
6
- let reporterConfig = undefined;
7
- function getReporter() {
8
- // if no config is set, return undefined
9
- if (!reporterConfig) {
10
- console.warn("initialising reporter without any config");
11
- }
12
- // initialise once config is set
13
- if (!reporterInstance && reporterConfig) {
14
- reporterInstance = new reporter_1.Reporter(reporterConfig);
15
- }
16
- return reporterInstance;
17
- }
18
- exports.getReporter = getReporter;
19
- function setReporterConfig(config) {
20
- console.info("initialised reporter config");
21
- reporterConfig = config;
22
- }
23
- exports.setReporterConfig = setReporterConfig;
File without changes