@empiricalrun/test-gen 0.25.2 → 0.26.0

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,15 @@
1
1
  # @empiricalrun/test-gen
2
2
 
3
+ ## 0.26.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 5882a20: feat: add inline master agent support
8
+
9
+ ### Patch Changes
10
+
11
+ - 5882a20: fix: improve code agent accuracy with updated prompt
12
+
3
13
  ## 0.25.2
4
14
 
5
15
  ### Patch Changes
@@ -43,7 +43,7 @@ class PlaywrightActions {
43
43
  }
44
44
  catch (e) {
45
45
  logger.log(`action: ${name} \nreason: ${args.reason}`);
46
- throw Error(`Error executing ${name} action of playwright: ${e}`);
46
+ throw Error(`Error executing ${name} action: ${e.message}`);
47
47
  }
48
48
  }
49
49
  getActionSchemas() {
@@ -1,2 +1,26 @@
1
- export declare function generateTestsUsingBrowsingAgent(testFilePath: string): Promise<void>;
1
+ type GenerateTestsType = {
2
+ /**
3
+ * Path to the test case file being updated or created
4
+ *
5
+ * @type {string}
6
+ */
7
+ testFilePath: string;
8
+ /**
9
+ * File path being updated for the concerned test case
10
+ *
11
+ * @type {string}
12
+ */
13
+ filePathToUpdate: string;
14
+ };
15
+ /**
16
+ *
17
+ * Function to generate tests using master agent
18
+ * @export
19
+ * @param {GenerateTestsType} {
20
+ * testFilePath,
21
+ * filePathToUpdate,
22
+ * }
23
+ */
24
+ export declare function generateTestsUsingMasterAgent({ testFilePath, filePathToUpdate, }: GenerateTestsType): Promise<void>;
25
+ export {};
2
26
  //# sourceMappingURL=run.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"run.d.ts","sourceRoot":"","sources":["../../../src/agent/browsing/run.ts"],"names":[],"mappings":"AAcA,wBAAsB,+BAA+B,CAAC,YAAY,EAAE,MAAM,iBA6BzE"}
1
+ {"version":3,"file":"run.d.ts","sourceRoot":"","sources":["../../../src/agent/browsing/run.ts"],"names":[],"mappings":"AAYA,KAAK,iBAAiB,GAAG;IACvB;;;;OAIG;IACH,YAAY,EAAE,MAAM,CAAC;IACrB;;;;OAIG;IACH,gBAAgB,EAAE,MAAM,CAAC;CAC1B,CAAC;AAEF;;;;;;;;GAQG;AACH,wBAAsB,6BAA6B,CAAC,EAClD,YAAY,EACZ,gBAAgB,GACjB,EAAE,iBAAiB,iBAoCnB"}
@@ -3,27 +3,39 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.generateTestsUsingBrowsingAgent = void 0;
6
+ exports.generateTestsUsingMasterAgent = void 0;
7
7
  const detect_port_1 = __importDefault(require("detect-port"));
8
- const logger_1 = require("../../bin/logger");
9
8
  const utils_1 = require("../../bin/utils");
10
9
  const web_1 = require("../../bin/utils/platform/web");
11
10
  const server_1 = require("../../file/server");
12
- const reporter_1 = require("../../reporter");
13
11
  const exec_1 = require("../../utils/exec");
14
12
  const utils_2 = require("./utils");
15
- async function generateTestsUsingBrowsingAgent(testFilePath) {
16
- const logger = new logger_1.CustomLogger({ useReporter: false });
17
- (0, utils_2.canRunBrowsingAgent)(testFilePath);
13
+ /**
14
+ *
15
+ * Function to generate tests using master agent
16
+ * @export
17
+ * @param {GenerateTestsType} {
18
+ * testFilePath,
19
+ * filePathToUpdate,
20
+ * }
21
+ */
22
+ async function generateTestsUsingMasterAgent({ testFilePath, filePathToUpdate, }) {
23
+ // valiate if the file path and file to update are valid
24
+ // also warn users if they are on older version of test-gen
25
+ (0, utils_2.canRunMasterAgent)(testFilePath);
26
+ // detect available http port on the machine
18
27
  const port = await (0, detect_port_1.default)(3030);
28
+ // start a file service to handle file updates from agent
29
+ // - also update the file path with updates when agent is done spitting out code
19
30
  const fileService = new server_1.FileService({ port });
20
31
  await fileService.startFileService();
21
- fileService.setFilePath(testFilePath);
32
+ fileService.setFilePath(filePathToUpdate);
22
33
  // read playwright config from ./playwright.config.ts of source repo
23
34
  const playwrightConfig = await (0, utils_2.readPlaywrightConfig)();
35
+ // detect the playwright project name for the given test file and playwright config
24
36
  const project = await (0, utils_2.detectProjectName)(testFilePath, playwrightConfig);
25
- logger.log(`Detected playwright project name: ${project}`);
26
- //TODO: change this to per test
37
+ console.log(`Detected playwright project name: ${project}`);
38
+ // run playwright test which will internally run the master agent
27
39
  let command = `npx playwright test ${testFilePath} --retries 0 --project ${project} --timeout 0`;
28
40
  if (!process.env.CI) {
29
41
  command = command.concat(` --headed`);
@@ -33,14 +45,16 @@ async function generateTestsUsingBrowsingAgent(testFilePath) {
33
45
  env: {
34
46
  APP_PORT: port.toString(),
35
47
  PW_TEST_HTML_REPORT_OPEN: "never",
48
+ // pass the test gen token so that the agent has the same configuration as cli
36
49
  TEST_GEN_TOKEN: (0, utils_1.getTestConfigCliArg)(),
37
50
  },
38
51
  });
39
52
  }
40
53
  catch (e) {
41
- logger.error(e);
42
- await new reporter_1.TestGenUpdatesReporter().sendMessage(e);
54
+ console.error(e);
43
55
  }
56
+ // remove test only from the test file in case of any error
44
57
  await (0, web_1.removeTestOnly)(testFilePath);
58
+ // TODO: remove the createTest function from the test file if its present
45
59
  }
46
- exports.generateTestsUsingBrowsingAgent = generateTestsUsingBrowsingAgent;
60
+ exports.generateTestsUsingMasterAgent = generateTestsUsingMasterAgent;
@@ -3,9 +3,23 @@ import { PlaywrightTestConfig } from "playwright/test";
3
3
  import { TestGenConfig } from "../../types";
4
4
  export declare function isRegExp(obj: any): obj is RegExp;
5
5
  export declare function prepareBrowsingAgentTask(steps: string[]): string;
6
- export declare function prepareFileForBrowsingAgent(genConfig: TestGenConfig): Promise<void>;
6
+ /**
7
+ * Function to prepare test file for master agent to run
8
+ * @param {TestGenConfig} genConfig
9
+ * @return {*} {Promise<string>}
10
+ */
11
+ export declare function prepareFileForMasterAgent(genConfig: TestGenConfig): Promise<string>;
7
12
  export declare function injectPwLocatorGenerator(page: Page): Promise<void>;
8
- export declare function canRunBrowsingAgent(filePath: string): void;
13
+ /**
14
+ * Function to validate if the test file path are valid.
15
+ * @throws if there are any missing dependencies for master agent to run.
16
+ * @param {string} filePath
17
+ */
18
+ export declare function canRunMasterAgent(filePath: string): void;
19
+ /**
20
+ * function to read playwright config from the source repo
21
+ * @return {*} {Promise<PlaywrightTestConfig>}
22
+ */
9
23
  export declare function readPlaywrightConfig(): Promise<PlaywrightTestConfig>;
10
24
  /**
11
25
  * detect the project name for the given file in playwright test repo
@@ -1 +1 @@
1
- {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../../src/agent/browsing/utils.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,IAAI,EAAE,MAAM,YAAY,CAAC;AAClC,OAAO,EAAE,oBAAoB,EAAE,MAAM,iBAAiB,CAAC;AAUvD,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAE5C,wBAAgB,QAAQ,CAAC,GAAG,EAAE,GAAG,GAAG,GAAG,IAAI,MAAM,CAKhD;AAED,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,MAAM,EAAE,UAIvD;AAED,wBAAsB,2BAA2B,CAAC,SAAS,EAAE,aAAa,iBA+CzE;AAwCD,wBAAsB,wBAAwB,CAAC,IAAI,EAAE,IAAI,iBAiBxD;AAED,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,MAAM,QA4BnD;AAED,wBAAsB,oBAAoB,IAAI,OAAO,CAAC,oBAAoB,CAAC,CAM1E;AAED;;;;;GAKG;AACH,wBAAsB,iBAAiB,CACrC,YAAY,EAAE,MAAM,EACpB,gBAAgB,EAAE,oBAAoB,GACrC,OAAO,CAAC,MAAM,CAAC,CAkDjB;AAED,wBAAsB,sBAAsB,CAAC,EAC3C,YAAiB,EACjB,IAAS,EACT,eAAoB,EACpB,gBAAqB,EACrB,UAAyC,GAC1C,EAAE;IACD,YAAY,EAAE,MAAM,CAAC;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,eAAe,EAAE,MAAM,EAAE,CAAC;IAC1B,gBAAgB,EAAE,MAAM,EAAE,CAAC;IAC3B,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,8EASA"}
1
+ {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../../src/agent/browsing/utils.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,IAAI,EAAE,MAAM,YAAY,CAAC;AAClC,OAAO,EAAE,oBAAoB,EAAE,MAAM,iBAAiB,CAAC;AAUvD,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAG5C,wBAAgB,QAAQ,CAAC,GAAG,EAAE,GAAG,GAAG,GAAG,IAAI,MAAM,CAKhD;AAED,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,MAAM,EAAE,UAIvD;AAyFD;;;;GAIG;AACH,wBAAsB,yBAAyB,CAC7C,SAAS,EAAE,aAAa,GACvB,OAAO,CAAC,MAAM,CAAC,CAuBjB;AAwCD,wBAAsB,wBAAwB,CAAC,IAAI,EAAE,IAAI,iBAiBxD;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,QA6BjD;AAED;;;GAGG;AACH,wBAAsB,oBAAoB,IAAI,OAAO,CAAC,oBAAoB,CAAC,CAM1E;AAED;;;;;GAKG;AACH,wBAAsB,iBAAiB,CACrC,YAAY,EAAE,MAAM,EACpB,gBAAgB,EAAE,oBAAoB,GACrC,OAAO,CAAC,MAAM,CAAC,CAkDjB;AAED,wBAAsB,sBAAsB,CAAC,EAC3C,YAAiB,EACjB,IAAS,EACT,eAAoB,EACpB,gBAAqB,EACrB,UAAyC,GAC1C,EAAE;IACD,YAAY,EAAE,MAAM,CAAC;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,eAAe,EAAE,MAAM,EAAE,CAAC;IAC1B,gBAAgB,EAAE,MAAM,EAAE,CAAC;IAC3B,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,8EASA"}
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.getPromptForNextAction = exports.detectProjectName = exports.readPlaywrightConfig = exports.canRunBrowsingAgent = exports.injectPwLocatorGenerator = exports.prepareFileForBrowsingAgent = exports.prepareBrowsingAgentTask = exports.isRegExp = void 0;
6
+ exports.getPromptForNextAction = exports.detectProjectName = exports.readPlaywrightConfig = exports.canRunMasterAgent = exports.injectPwLocatorGenerator = exports.prepareFileForMasterAgent = exports.prepareBrowsingAgentTask = exports.isRegExp = void 0;
7
7
  const llm_1 = require("@empiricalrun/llm");
8
8
  const child_process_1 = require("child_process");
9
9
  const fs_extra_1 = __importDefault(require("fs-extra"));
@@ -11,6 +11,7 @@ const minimatch_1 = require("minimatch");
11
11
  const api_1 = __importDefault(require("tsx/cjs/api"));
12
12
  const logger_1 = require("../../bin/logger");
13
13
  const web_1 = require("../../bin/utils/platform/web");
14
+ const update_flow_1 = require("../codegen/update-flow");
14
15
  function isRegExp(obj) {
15
16
  return (obj instanceof RegExp ||
16
17
  Object.prototype.toString.call(obj) === "[object RegExp]");
@@ -22,36 +23,91 @@ function prepareBrowsingAgentTask(steps) {
22
23
  return task;
23
24
  }
24
25
  exports.prepareBrowsingAgentTask = prepareBrowsingAgentTask;
25
- async function prepareFileForBrowsingAgent(genConfig) {
26
+ /**
27
+ * Function to prepare test file for update scenarios for master agent to run
28
+ * @param {TestGenConfig} genConfig
29
+ */
30
+ async function prepareFileForUpdateScenario(genConfig) {
31
+ const { specPath, testCase } = genConfig;
32
+ const { name, steps } = testCase;
33
+ // update the test case with appropriate location for createTest
34
+ // TODO: reduce the payload for this LLM call. Only provide test file and page files which are used in the test block
35
+ // this will help with faster response
36
+ const [suggestion] = await (0, update_flow_1.updateTest)({
37
+ ...testCase,
38
+ steps: [
39
+ ...steps,
40
+ `
41
+ - You are given a method with interface await createTest(<task>, <playwright_page_instance>)
42
+ - This method will help you execute given task.
43
+ - Given the task you need to decide the right file path and code block to place this method.
44
+ - The task should contain hints as in where to place this method.
45
+ - If there is no hint provided in the task, you can assume the method needs to be placed at the end of the test block.
46
+ - YOU NEED TO MANDATORILY USE "createTest" method to execute the task.
47
+ - Once the methods is placed in the right location with a task and playwright page provided, assume the task is done.
48
+ - You need to respond with file path, code block and updated code block where this method can be placed in order to accomplish the task`,
49
+ ],
50
+ }, specPath, genConfig.options, false);
51
+ const createTestFilePath = suggestion?.updatedFiles[0] || "";
52
+ console.log("appending to existing test block");
53
+ console.log("updated test file path", createTestFilePath);
54
+ // if change suggested by LLM is in spec file
55
+ const isChangeInSpecFile = createTestFilePath.includes("spec.ts");
56
+ // if the file is not a spec file, add import for the test-gen
57
+ if (!isChangeInSpecFile) {
58
+ await fs_extra_1.default.writeFile(createTestFilePath, (0, web_1.addNewImport)(await fs_extra_1.default.readFile(createTestFilePath, "utf-8"), ["createTest"], "@empiricalrun/test-gen"));
59
+ }
60
+ const testFileContent = await fs_extra_1.default.readFile(specPath, "utf-8");
61
+ const { testBlock, testNode } = (0, web_1.getTypescriptTestBlock)(name, testFileContent);
62
+ const parentDescribe = (0, web_1.findFirstSerialDescribeBlock)(testNode);
63
+ // add test.only / describe.only to the spec file so that only that block is executed
64
+ const updatedTestFileContent = newContentsWithTestOnly(testFileContent, testBlock, testBlock, parentDescribe?.getText() || "");
65
+ await fs_extra_1.default.writeFile(specPath, isChangeInSpecFile
66
+ ? (0, web_1.addNewImport)(updatedTestFileContent, ["createTest"], "@empiricalrun/test-gen")
67
+ : updatedTestFileContent);
68
+ }
69
+ /**
70
+ * Function to prepare test file for new scenarios for master agent to run
71
+ * @param {TestGenConfig} genConfig
72
+ */
73
+ async function prepareFileForNewScenario(genConfig) {
26
74
  const { specPath, testCase } = genConfig;
27
75
  const { name, steps } = testCase;
28
- const logger = new logger_1.CustomLogger({ useReporter: false });
76
+ console.log("creating new test block");
77
+ const mergedSteps = prepareBrowsingAgentTask(steps);
78
+ // TODO: this assumes that test code repo has `page` as the main entrypoint fixture
79
+ const testGenCodeBlock = createTestBlockForCreateTest(name, mergedSteps);
80
+ const existingContents = await fs_extra_1.default.readFile(specPath, "utf-8");
81
+ const newContents = `${existingContents}\n\n${testGenCodeBlock}`;
82
+ await fs_extra_1.default.writeFile(specPath, (0, web_1.addNewImport)(newContents, ["createTest"], "@empiricalrun/test-gen"));
83
+ }
84
+ /**
85
+ * Function to prepare test file for master agent to run
86
+ * @param {TestGenConfig} genConfig
87
+ * @return {*} {Promise<string>}
88
+ */
89
+ async function prepareFileForMasterAgent(genConfig) {
90
+ const { specPath, testCase } = genConfig;
91
+ const { name } = testCase;
92
+ // check if the spec file exists
93
+ // if no then create a new file with test and expect imports
29
94
  if (!fs_extra_1.default.existsSync(specPath)) {
30
95
  await fs_extra_1.default.createFile(specPath);
31
96
  const fileContentWithImports = (0, web_1.addNewImport)("", ["test", "expect"], (0, web_1.getFixtureImportPath)(specPath));
32
97
  await fs_extra_1.default.writeFile(specPath, fileContentWithImports, "utf-8");
33
98
  }
34
- if (name && steps && steps.length) {
35
- const existingContents = await fs_extra_1.default.readFile(specPath, "utf-8");
36
- const { testBlock, parentDescribe } = (0, web_1.getTypescriptTestBlock)(name, existingContents);
37
- const mergedSteps = prepareBrowsingAgentTask(steps);
38
- let newContents = existingContents;
39
- if (testBlock) {
40
- logger.log("appending to existing test block");
41
- // test scenario is already present
42
- const updatedTestBlock = (0, web_1.appendToTestBlock)(testBlock, createTestGenBlock(mergedSteps));
43
- newContents = newContentsWithTestOnly(existingContents, testBlock, updatedTestBlock, parentDescribe);
44
- }
45
- else {
46
- logger.log("creating new test block");
47
- // TODO: this assumes that test code repo has `page` as the main entrypoint fixture
48
- const testGenCodeBlock = createTestBlockForCreateTest(name, mergedSteps);
49
- newContents = `${existingContents}\n\n${testGenCodeBlock}`;
50
- }
51
- await fs_extra_1.default.writeFile(specPath, (0, web_1.addNewImport)(newContents, ["createTest"], "@empiricalrun/test-gen"));
99
+ let createTestFilePath = specPath;
100
+ const existingContents = await fs_extra_1.default.readFile(specPath, "utf-8");
101
+ const { testBlock } = (0, web_1.getTypescriptTestBlock)(name, existingContents);
102
+ if (testBlock) {
103
+ await prepareFileForUpdateScenario(genConfig);
104
+ }
105
+ else {
106
+ await prepareFileForNewScenario(genConfig);
52
107
  }
108
+ return createTestFilePath;
53
109
  }
54
- exports.prepareFileForBrowsingAgent = prepareFileForBrowsingAgent;
110
+ exports.prepareFileForMasterAgent = prepareFileForMasterAgent;
55
111
  function newContentsWithTestOnly(existingContents, originalTestBlock, updatedTestBlock, parentDescribeBlock) {
56
112
  if (!parentDescribeBlock) {
57
113
  const testMarkedAsOnly = updatedTestBlock.replace("test(", "test.only(");
@@ -95,9 +151,14 @@ async function injectPwLocatorGenerator(page) {
95
151
  await Promise.all(scripts.map((s) => page.addScriptTag({ content: s })));
96
152
  }
97
153
  exports.injectPwLocatorGenerator = injectPwLocatorGenerator;
98
- function canRunBrowsingAgent(filePath) {
154
+ /**
155
+ * Function to validate if the test file path are valid.
156
+ * @throws if there are any missing dependencies for master agent to run.
157
+ * @param {string} filePath
158
+ */
159
+ function canRunMasterAgent(filePath) {
99
160
  if (!fs_extra_1.default.existsSync(filePath)) {
100
- throw new Error(`File for browsing agent to run not found: ${filePath}`);
161
+ throw new Error(`File for master agent to run not found: ${filePath}`);
101
162
  }
102
163
  const { dependencies: installedPackages } = JSON.parse((0, child_process_1.execSync)("npm ls --json").toString());
103
164
  const hasPwTestInstalled = installedPackages["@playwright/test"];
@@ -115,7 +176,11 @@ function canRunBrowsingAgent(filePath) {
115
176
  logger.warn(`Outdated @empiricalrun/test-gen package: expected ${latestTestGenVersion}, got ${testGenVersion}`);
116
177
  }
117
178
  }
118
- exports.canRunBrowsingAgent = canRunBrowsingAgent;
179
+ exports.canRunMasterAgent = canRunMasterAgent;
180
+ /**
181
+ * function to read playwright config from the source repo
182
+ * @return {*} {Promise<PlaywrightTestConfig>}
183
+ */
119
184
  async function readPlaywrightConfig() {
120
185
  const [lastDir] = process.cwd().split("/").reverse();
121
186
  const playwrightConfig = (await api_1.default.require("./playwright.config.ts", `${process.cwd()}/${lastDir}`)).default;
@@ -1 +1 @@
1
- {"version":3,"file":"run.d.ts","sourceRoot":"","sources":["../../../src/agent/codegen/run.ts"],"names":[],"mappings":"AAoBA,OAAO,EAAE,QAAQ,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AAG7D,wBAAsB,YAAY,CAChC,QAAQ,EAAE,QAAQ,EAClB,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,oBAAoB,GAC5B,OAAO,CAAC,QAAQ,EAAE,CAAC,CAmJrB"}
1
+ {"version":3,"file":"run.d.ts","sourceRoot":"","sources":["../../../src/agent/codegen/run.ts"],"names":[],"mappings":"AAyBA,OAAO,EAAE,QAAQ,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AAG7D,wBAAsB,YAAY,CAChC,QAAQ,EAAE,QAAQ,EAClB,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,oBAAoB,GAC5B,OAAO,CAAC,QAAQ,EAAE,CAAC,CAoJrB"}
@@ -142,6 +142,7 @@ async function generateTest(testCase, file, options) {
142
142
  logger.log(`Trace: ${trace.getTraceUrl()}`);
143
143
  generatedTestCases.push(testCase);
144
144
  trace.update({ input: { testCase }, output: { response } });
145
+ await (0, llm_1.flushAllTraces)();
145
146
  return generatedTestCases;
146
147
  }
147
148
  exports.generateTest = generateTest;
@@ -1,3 +1,7 @@
1
1
  import { TestCase, TestGenConfigOptions } from "../../types";
2
- export declare function updateTest(testCase: TestCase, file: string, options: TestGenConfigOptions): Promise<TestCase[]>;
2
+ type UpdatedTestCase = TestCase & {
3
+ updatedFiles: string[];
4
+ };
5
+ export declare function updateTest(testCase: TestCase, file: string, options: TestGenConfigOptions | undefined, logging?: boolean): Promise<UpdatedTestCase[]>;
6
+ export {};
3
7
  //# sourceMappingURL=update-flow.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"update-flow.d.ts","sourceRoot":"","sources":["../../../src/agent/codegen/update-flow.ts"],"names":[],"mappings":"AAkBA,OAAO,EAAE,QAAQ,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AAoB7D,wBAAsB,UAAU,CAC9B,QAAQ,EAAE,QAAQ,EAClB,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,oBAAoB,GAC5B,OAAO,CAAC,QAAQ,EAAE,CAAC,CAiGrB"}
1
+ {"version":3,"file":"update-flow.d.ts","sourceRoot":"","sources":["../../../src/agent/codegen/update-flow.ts"],"names":[],"mappings":"AAwBA,OAAO,EAAE,QAAQ,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AAqB7D,KAAK,eAAe,GAAG,QAAQ,GAAG;IAChC,YAAY,EAAE,MAAM,EAAE,CAAC;CACxB,CAAC;AAEF,wBAAsB,UAAU,CAC9B,QAAQ,EAAE,QAAQ,EAClB,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,oBAAoB,GAAG,SAAS,EACzC,OAAO,GAAE,OAAc,GACtB,OAAO,CAAC,eAAe,EAAE,CAAC,CAoH5B"}
@@ -14,7 +14,8 @@ const constants_1 = require("../../constants");
14
14
  const session_1 = require("../../session");
15
15
  function extractInformation(input) {
16
16
  const result = [];
17
- const regex = /<filepath>(.*?)<\/filepath.*?>[\s\S]*?<old_code_block>([\s\S]*?)<\/old_code_block>[\s\S]*?<new_code_block>([\s\S]*?)<\/new_code_block>[\s\S]*?<reason>([\s\S]*?)<\/reason>/g;
17
+ // TODO: use better structure for this. Do not kill me for this please.
18
+ const regex = /<file_path>(.*?)<\/file_path>[\s\S]*?<old_code_block>([\s\S]*?)<\/old_code_block>[\s\S]*?<new_code_block>([\s\S]*?)<\/new_code_block>[\s\S]*?<change>([\s\S]*?)<\/change>/g;
18
19
  let match;
19
20
  while ((match = regex.exec(input)) !== null) {
20
21
  const [, filePath, oldCode, newCode, reason] = match;
@@ -27,8 +28,8 @@ function extractInformation(input) {
27
28
  }
28
29
  return result;
29
30
  }
30
- async function updateTest(testCase, file, options) {
31
- const logger = new logger_1.CustomLogger();
31
+ async function updateTest(testCase, file, options, logging = true) {
32
+ const logger = new logger_1.CustomLogger({ useReporter: logging });
32
33
  const context = await (0, context_1.contextForGeneration)(file);
33
34
  const { codePrompt, pomPrompt, testFileContent } = context;
34
35
  const generatedTestCases = [];
@@ -38,7 +39,10 @@ async function updateTest(testCase, file, options) {
38
39
  name: "update-test",
39
40
  id: crypto_1.default.randomUUID(),
40
41
  release: session.version,
41
- tags: [options.metadata.projectName, options.metadata.environment].filter((s) => !!s),
42
+ tags: [
43
+ options?.metadata.projectName || "",
44
+ options?.metadata.environment || "",
45
+ ].filter((s) => !!s),
42
46
  });
43
47
  trace.event({
44
48
  name: "collate-files-as-text",
@@ -59,30 +63,34 @@ async function updateTest(testCase, file, options) {
59
63
  scenarioName: testCase.name,
60
64
  scenarioSteps: testCase.steps.join("\n"),
61
65
  scenarioFile: file,
62
- }, 8);
66
+ }, 14);
63
67
  promptSpan.end({ output: { instruction } });
64
68
  const llm = new llm_1.LLM({
65
69
  trace,
66
- provider: options.modelProvider || constants_1.DEFAULT_MODEL_PROVIDER,
67
- defaultModel: options.model || constants_1.DEFAULT_MODEL,
68
- providerApiKey: constants_1.MODEL_API_KEYS[options.modelProvider || constants_1.DEFAULT_MODEL_PROVIDER],
70
+ provider: options?.modelProvider || constants_1.DEFAULT_MODEL_PROVIDER,
71
+ defaultModel: options?.model || constants_1.DEFAULT_MODEL,
72
+ providerApiKey: constants_1.MODEL_API_KEYS[options?.modelProvider || constants_1.DEFAULT_MODEL_PROVIDER],
69
73
  });
70
74
  const firstShotMessage = await llm.createChatCompletion({
71
75
  messages: instruction,
72
76
  modelParameters: {
73
77
  ...constants_1.DEFAULT_MODEL_PARAMETERS,
74
- ...options.modelParameters,
78
+ ...options?.modelParameters,
75
79
  },
76
80
  });
77
81
  let response = firstShotMessage?.content || "";
78
82
  logger.success("Test generated successfully!");
79
83
  const fileChanges = extractInformation(response);
80
- fileChanges.forEach(async (fileChange) => {
81
- if (fileChange.filePath?.includes("spec.ts")) {
84
+ await Promise.allSettled(fileChanges.map(async (fileChange) => {
85
+ if (!fileChange.filePath) {
86
+ return;
87
+ }
88
+ const { testBlock: testBlockUpdate } = (0, web_1.getTypescriptTestBlock)(testCase?.name || "", fileChange.newCode || "");
89
+ if (testBlockUpdate) {
82
90
  // assuming the test case getting updated
83
91
  // maintaining the previous accuracy of the test case update
84
92
  const readWriteFileSpan = trace.span({ name: "write-to-file" });
85
- let contents = fs_extra_1.default.readFileSync(fileChange.filePath, "utf-8");
93
+ let contents = await fs_extra_1.default.readFile(fileChange.filePath, "utf-8");
86
94
  const [prependContent, strippedContent] = await (0, web_1.stripAndPrependImports)(fileChange.newCode, testCase?.name);
87
95
  let updatedContent = prependContent + contents + `\n\n${strippedContent}`;
88
96
  const { testBlock } = (0, web_1.getTypescriptTestBlock)(testCase?.name, contents);
@@ -91,25 +99,33 @@ async function updateTest(testCase, file, options) {
91
99
  await fs_extra_1.default.writeFile(file, updatedContent, "utf-8");
92
100
  readWriteFileSpan.end({ output: { updatedContent } });
93
101
  trace.event({ name: "format-file" });
102
+ await (0, web_1.lintErrors)(fileChange.filePath);
94
103
  await (0, web_1.formatCode)(fileChange.filePath);
95
- logger.success("File formatted successfully!");
104
+ logger.success(`${fileChange.filePath} file formatted successfully!`);
96
105
  }
97
106
  else {
98
107
  // since we dont know what is getting updated,
99
- // we believe that the patch is correct and contains few before and after lines to make the change unique
108
+ // we believe that the patch is correct and contains few before and after lines
109
+ // to make the change unique for search & replace
100
110
  const readWriteFileSpan = trace.span({ name: "write-to-file" });
101
- let contents = fs_extra_1.default.readFileSync(fileChange.filePath, "utf-8");
111
+ let contents = await fs_extra_1.default.readFile(fileChange.filePath, "utf-8");
112
+ //TODO: move this to usage of ast blocks
102
113
  contents = contents.replace(fileChange.oldCode, `\n\n${fileChange.newCode}`);
103
114
  await fs_extra_1.default.writeFile(fileChange.filePath, contents, "utf-8");
104
115
  readWriteFileSpan.end({ output: { contents } });
105
116
  trace.event({ name: "format-file" });
117
+ await (0, web_1.lintErrors)(fileChange.filePath);
106
118
  await (0, web_1.formatCode)(fileChange.filePath);
107
- logger.success("File formatted successfully!");
119
+ logger.success(`${fileChange.filePath} file formatted successfully!`);
108
120
  }
109
- });
121
+ }));
110
122
  logger.log(`Trace: ${trace.getTraceUrl()}`);
111
- generatedTestCases.push(testCase);
123
+ generatedTestCases.push({
124
+ ...testCase,
125
+ updatedFiles: fileChanges.map((f) => f.filePath),
126
+ });
112
127
  trace.update({ input: { testCase }, output: { response } });
128
+ await (0, llm_1.flushAllTraces)();
113
129
  return generatedTestCases;
114
130
  }
115
131
  exports.updateTest = updateTest;
package/dist/bin/index.js CHANGED
@@ -27,8 +27,11 @@ async function runAgent(testGenConfig) {
27
27
  if (testGenConfig.options?.agent !== "code") {
28
28
  // this assumes we have only one scenario in test config
29
29
  logger.success(`Generating test using ${testGenConfig.options?.agent} agent`);
30
- await (0, utils_1.prepareFileForBrowsingAgent)(testGenConfig);
31
- await (0, run_1.generateTestsUsingBrowsingAgent)(specPath);
30
+ const filePathToUpdate = await (0, utils_1.prepareFileForMasterAgent)(testGenConfig);
31
+ await (0, run_1.generateTestsUsingMasterAgent)({
32
+ testFilePath: specPath,
33
+ filePathToUpdate,
34
+ });
32
35
  await new reporter_1.TestGenUpdatesReporter().reportGenAssets({
33
36
  projectRepoName: testGenConfig.options.metadata.projectRepoName,
34
37
  testName: testCase.name,
@@ -11,14 +11,14 @@ async function contextForGeneration(file) {
11
11
  const ignoreFn = (0, ignore_1.default)();
12
12
  if (fs_extra_1.default.existsSync(".gitignore")) {
13
13
  // Not checking for nested gitignore
14
- const gitignore = fs_extra_1.default.readFileSync(".gitignore").toString();
14
+ const gitignore = (await fs_extra_1.default.readFile(".gitignore")).toString();
15
15
  ignoreFn.add(gitignore);
16
16
  }
17
17
  const filter = ignoreFn.createFilter();
18
18
  return {
19
19
  codePrompt: await (0, fs_1.generatePromptFromDirectory)("./tests", filter),
20
20
  pomPrompt: await (0, fs_1.generatePromptFromDirectory)("./pages", filter),
21
- testFileContent: fs_extra_1.default.readFileSync(file, "utf-8"),
21
+ testFileContent: await fs_extra_1.default.readFile(file, "utf-8"),
22
22
  };
23
23
  }
24
24
  exports.contextForGeneration = contextForGeneration;
@@ -1,7 +1,39 @@
1
+ import { Node } from "ts-morph";
2
+ /**
3
+ * function to get the test block and test node for the scenario
4
+ * @export
5
+ * @param {string} scenarioName
6
+ * @param {string} content
7
+ * @return { testBlock: string; parentDescribe: string; } testBlock - the test block content, testNode - the test function node
8
+ */
1
9
  export declare function getTypescriptTestBlock(scenarioName: string, content: string): {
2
10
  testBlock: string | undefined;
3
- parentDescribe: string | undefined;
11
+ testNode: Node | undefined;
4
12
  };
13
+ /**
14
+ * Function to find the first 'describe' block configured with 'serial: true'
15
+ *
16
+ * e.g.
17
+ *
18
+ * test.describe("foo", () => {
19
+ *
20
+ * test.describe.configure({ mode: "serial" });
21
+ *
22
+ * test.describe("bar", () => {
23
+ *
24
+ * test.describe.configure({ mode: "serial" });
25
+ *
26
+ * })
27
+ *
28
+ * })
29
+ *
30
+ * for the above example,
31
+ * this function will return the first 'describe' block which is named "foo"
32
+ *
33
+ * @param {(Node | undefined)} node
34
+ * @return {(Node | undefined)}
35
+ */
36
+ export declare function findFirstSerialDescribeBlock(node: Node | undefined): Node | undefined;
5
37
  export declare function appendToTestBlock(testBlock: string, content: string): string;
6
38
  export declare function validateTypescript(filePath: string): string[];
7
39
  export declare function stripAndPrependImports(content: string, testName: string): Promise<(string | undefined)[]>;
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/bin/utils/platform/web/index.ts"],"names":[],"mappings":"AAMA,wBAAgB,sBAAsB,CAAC,YAAY,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM;;;EAwB3E;AAED,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAG5E;AAED,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,EAAE,CAwD7D;AAED,wBAAsB,sBAAsB,CAC1C,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,MAAM,mCAUjB;AAED,wBAAsB,UAAU,CAAC,QAAQ,EAAE,MAAM,iBAShD;AAED,wBAAsB,UAAU,CAAC,QAAQ,EAAE,MAAM,iBAQhD;AAED,wBAAgB,YAAY,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,EAAE,GAAG,EAAE,MAAM,UAE5E;AAED,wBAAsB,cAAc,CAAC,QAAQ,EAAE,MAAM,iBAMpD;AAED,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,MAAM,UAcpD;AAED,wBAAgB,4BAA4B,CAC1C,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,EAChB,aAAa,EAAE,MAAM,UA0CtB"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/bin/utils/platform/web/index.ts"],"names":[],"mappings":"AAGA,OAAO,EAAqB,IAAI,EAAuB,MAAM,UAAU,CAAC;AAGxE;;;;;;GAMG;AACH,wBAAgB,sBAAsB,CACpC,YAAY,EAAE,MAAM,EACpB,OAAO,EAAE,MAAM,GACd;IACD,SAAS,EAAE,MAAM,GAAG,SAAS,CAAC;IAC9B,QAAQ,EAAE,IAAI,GAAG,SAAS,CAAC;CAC5B,CAeA;AAED;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,4BAA4B,CAC1C,IAAI,EAAE,IAAI,GAAG,SAAS,GACrB,IAAI,GAAG,SAAS,CA4BlB;AAED,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAG5E;AAED,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,EAAE,CAwD7D;AAED,wBAAsB,sBAAsB,CAC1C,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,MAAM,mCAUjB;AAED,wBAAsB,UAAU,CAAC,QAAQ,EAAE,MAAM,iBAShD;AAED,wBAAsB,UAAU,CAAC,QAAQ,EAAE,MAAM,iBAQhD;AAED,wBAAgB,YAAY,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,EAAE,GAAG,EAAE,MAAM,UAE5E;AAED,wBAAsB,cAAc,CAAC,QAAQ,EAAE,MAAM,iBAMpD;AAED,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,MAAM,UAcpD;AAED,wBAAgB,4BAA4B,CAC1C,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,EAChB,aAAa,EAAE,MAAM,UA0CtB"}
@@ -3,28 +3,78 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.replaceCreateTestWithNewCode = exports.getFixtureImportPath = exports.removeTestOnly = exports.addNewImport = exports.formatCode = exports.lintErrors = exports.stripAndPrependImports = exports.validateTypescript = exports.appendToTestBlock = exports.getTypescriptTestBlock = void 0;
6
+ exports.replaceCreateTestWithNewCode = exports.getFixtureImportPath = exports.removeTestOnly = exports.addNewImport = exports.formatCode = exports.lintErrors = exports.stripAndPrependImports = exports.validateTypescript = exports.appendToTestBlock = exports.findFirstSerialDescribeBlock = exports.getTypescriptTestBlock = void 0;
7
7
  const eslint_1 = require("eslint");
8
8
  const fs_extra_1 = __importDefault(require("fs-extra"));
9
9
  const prettier_1 = __importDefault(require("prettier"));
10
10
  const ts_morph_1 = require("ts-morph");
11
11
  const typescript_1 = __importDefault(require("typescript"));
12
+ /**
13
+ * function to get the test block and test node for the scenario
14
+ * @export
15
+ * @param {string} scenarioName
16
+ * @param {string} content
17
+ * @return { testBlock: string; parentDescribe: string; } testBlock - the test block content, testNode - the test function node
18
+ */
12
19
  function getTypescriptTestBlock(scenarioName, content) {
13
20
  const project = new ts_morph_1.Project();
14
21
  const sourceFile = project.createSourceFile("test.ts", content);
15
22
  const testFunctionNode = sourceFile.getFirstDescendant((node) => !!(node.isKind(ts_morph_1.SyntaxKind.CallExpression) &&
16
23
  node.getExpression().getText() === "test" &&
17
24
  node.getArguments()[0]?.getText().includes(scenarioName)));
18
- // TODO: support files that have 2 describe blocks with the same test name in them
19
- const describeBlockNode = sourceFile.getFirstDescendant((node) => !!(node.isKind(ts_morph_1.SyntaxKind.CallExpression) &&
20
- node.getExpression().getText() === "test.describe" &&
21
- node.getText().includes(scenarioName)));
22
25
  return {
23
26
  testBlock: testFunctionNode?.getText(),
24
- parentDescribe: describeBlockNode?.getText(),
27
+ testNode: testFunctionNode,
25
28
  };
26
29
  }
27
30
  exports.getTypescriptTestBlock = getTypescriptTestBlock;
31
+ /**
32
+ * Function to find the first 'describe' block configured with 'serial: true'
33
+ *
34
+ * e.g.
35
+ *
36
+ * test.describe("foo", () => {
37
+ *
38
+ * test.describe.configure({ mode: "serial" });
39
+ *
40
+ * test.describe("bar", () => {
41
+ *
42
+ * test.describe.configure({ mode: "serial" });
43
+ *
44
+ * })
45
+ *
46
+ * })
47
+ *
48
+ * for the above example,
49
+ * this function will return the first 'describe' block which is named "foo"
50
+ *
51
+ * @param {(Node | undefined)} node
52
+ * @return {(Node | undefined)}
53
+ */
54
+ function findFirstSerialDescribeBlock(node) {
55
+ let currentNode = node;
56
+ // Traverse upwards until we find a 'describe' block with 'serial: true'
57
+ while (currentNode) {
58
+ const parentDescribe = currentNode.getFirstAncestorByKind(ts_morph_1.SyntaxKind.CallExpression);
59
+ if (parentDescribe) {
60
+ const isDescribe = parentDescribe.getFirstChild()?.getText() === "test.describe";
61
+ if (isDescribe) {
62
+ const configureCall = parentDescribe
63
+ .getDescendantsOfKind(ts_morph_1.SyntaxKind.CallExpression)
64
+ .find((call) => call.getText().includes("configure"));
65
+ if (configureCall) {
66
+ // Check if 'serial: true' exists
67
+ if (configureCall.getText().includes("serial")) {
68
+ return parentDescribe;
69
+ }
70
+ }
71
+ }
72
+ }
73
+ currentNode = parentDescribe; // Move up the tree
74
+ }
75
+ return undefined; // Return undefined if no 'describe' with serial: true is found
76
+ }
77
+ exports.findFirstSerialDescribeBlock = findFirstSerialDescribeBlock;
28
78
  function appendToTestBlock(testBlock, content) {
29
79
  const updateTestBlock = testBlock.replace(/\}\)$/, `\n\n${content}\n\n })`);
30
80
  return updateTestBlock;
@@ -96,7 +146,7 @@ async function lintErrors(filePath) {
96
146
  });
97
147
  const [result] = await eslint.lintFiles(filePath);
98
148
  if (result?.output) {
99
- fs_extra_1.default.writeFileSync(filePath, result.output);
149
+ await fs_extra_1.default.writeFile(filePath, result.output);
100
150
  }
101
151
  }
102
152
  exports.lintErrors = lintErrors;
@@ -107,7 +157,7 @@ async function formatCode(filePath) {
107
157
  ...prettierConfig,
108
158
  filepath: filePath,
109
159
  });
110
- fs_extra_1.default.writeFileSync(filePath, formattedContent);
160
+ await fs_extra_1.default.writeFile(filePath, formattedContent);
111
161
  }
112
162
  exports.formatCode = formatCode;
113
163
  function addNewImport(contents, modules, pkg) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@empiricalrun/test-gen",
3
- "version": "0.25.2",
3
+ "version": "0.26.0",
4
4
  "publishConfig": {
5
5
  "registry": "https://registry.npmjs.org/",
6
6
  "access": "public"