@alwaysmeticulous/cli 2.38.0 → 2.40.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.
Files changed (27) hide show
  1. package/dist/commands/bootstrap/bootstrap.command.js +1 -1
  2. package/dist/commands/create-test/create-test.command.d.ts +0 -4
  3. package/dist/commands/create-test/create-test.command.js +1 -6
  4. package/dist/commands/replay/replay.command.d.ts +1 -6
  5. package/dist/commands/replay/replay.command.js +1 -6
  6. package/dist/commands/run-all-tests/run-all-tests.command.d.ts +5 -5
  7. package/dist/commands/run-all-tests/run-all-tests.command.js +7 -8
  8. package/dist/commands/screenshot-diff/utils/__tests__/get-screenshot-identifier.spec.d.ts +1 -0
  9. package/dist/commands/screenshot-diff/utils/__tests__/get-screenshot-identifier.spec.js +25 -0
  10. package/dist/commands/screenshot-diff/utils/get-screenshot-filename.js +6 -2
  11. package/dist/commands/screenshot-diff/utils/get-screenshot-identifier.js +16 -1
  12. package/dist/config/config.js +1 -2
  13. package/dist/parallel-tests/__tests__/merge-test-results.spec.js +4 -2
  14. package/dist/parallel-tests/__tests__/parrallel-tests.handler.spec.js +30 -0
  15. package/dist/parallel-tests/merge-test-results.d.ts +3 -1
  16. package/dist/parallel-tests/merge-test-results.js +19 -23
  17. package/dist/parallel-tests/messages.types.d.ts +2 -2
  18. package/dist/parallel-tests/parallel-replay.handler.d.ts +3 -0
  19. package/dist/{deflake-tests/deflake-tests.handler.js → parallel-tests/parallel-replay.handler.js} +3 -23
  20. package/dist/{deflake-tests/deflake-tests.handler.d.ts → parallel-tests/parallel-replay.types.d.ts} +1 -7
  21. package/dist/parallel-tests/parallel-replay.types.js +2 -0
  22. package/dist/parallel-tests/parallel-tests.handler.d.ts +1 -0
  23. package/dist/parallel-tests/parallel-tests.handler.js +30 -7
  24. package/dist/parallel-tests/run-all-tests.d.ts +11 -2
  25. package/dist/parallel-tests/run-all-tests.js +3 -7
  26. package/dist/parallel-tests/task.handler.js +2 -2
  27. package/package.json +4 -4
@@ -22,7 +22,7 @@ const handler = async () => {
22
22
  logger.info(`Setting up ${chalk_1.default.green("test:meticulous")} script...`);
23
23
  await (0, npm_set_script_utils_1.npmSetScript)({
24
24
  script: "test:meticulous",
25
- command: "meticulous run-all-tests --headless --parallelize --deflake",
25
+ command: "meticulous run-all-tests --headless --parallelize",
26
26
  });
27
27
  };
28
28
  exports.bootstrapCommand = (0, command_builder_1.buildCommand)("bootstrap")
@@ -78,10 +78,6 @@ export declare const createTestCommand: import("yargs").CommandModule<unknown, i
78
78
  boolean: true;
79
79
  description: string;
80
80
  };
81
- screenshotSelector: {
82
- string: true;
83
- description: string;
84
- };
85
81
  moveBeforeClick: {
86
82
  readonly boolean: true;
87
83
  readonly description: "Simulate mouse movement before clicking";
@@ -59,7 +59,7 @@ apiToken, commitSha, devTools, bypassCSP,
59
59
  // Record options
60
60
  width, height, uploadIntervalMs, incognito, trace,
61
61
  // Replay options
62
- headless, screenshotSelector, shiftTime, networkStubbing, moveBeforeClick, cookiesFile, disableRemoteFonts, noSandbox, skipPauses, }) => {
62
+ headless, shiftTime, networkStubbing, moveBeforeClick, cookiesFile, disableRemoteFonts, noSandbox, skipPauses, }) => {
63
63
  const logger = loglevel_1.default.getLogger(common_1.METICULOUS_LOGGER_NAME);
64
64
  logger.info("Creating a new Meticulous test");
65
65
  logger.info("Step 1: record a new test");
@@ -95,7 +95,6 @@ headless, screenshotSelector, shiftTime, networkStubbing, moveBeforeClick, cooki
95
95
  devTools,
96
96
  bypassCSP,
97
97
  screenshot: true,
98
- screenshotSelector,
99
98
  shiftTime,
100
99
  disableRemoteFonts,
101
100
  noSandbox,
@@ -148,10 +147,6 @@ exports.createTestCommand = (0, command_builder_1.buildCommand)("create-test")
148
147
  description: "Enable verbose logging",
149
148
  },
150
149
  // Replay options
151
- screenshotSelector: {
152
- string: true,
153
- description: "Query selector to screenshot a specific DOM element instead of the whole page",
154
- },
155
150
  moveBeforeClick: common_options_1.OPTIONS.moveBeforeClick,
156
151
  cookiesFile: {
157
152
  string: true,
@@ -23,7 +23,6 @@ export interface RawReplayCommandHandlerOptions extends ScreenshotDiffOptions, O
23
23
  screenshot: boolean;
24
24
  appUrl: string | null | undefined;
25
25
  simulationIdForAssets: string | null | undefined;
26
- screenshotSelector: string | null | undefined;
27
26
  maxDurationMs: number | null | undefined;
28
27
  maxEventCount: number | null | undefined;
29
28
  storyboard: boolean;
@@ -36,7 +35,7 @@ interface AdditionalReplayOptions {
36
35
  cookiesFile: string | null | undefined;
37
36
  debugger: boolean;
38
37
  }
39
- export declare const rawReplayCommandHandler: ({ apiToken, commitSha, sessionId, appUrl, simulationIdForAssets, headless, devTools, bypassCSP, screenshot, screenshotSelector, diffThreshold, diffPixelThreshold, shiftTime, networkStubbing, moveBeforeClick, cookiesFile, disableRemoteFonts, noSandbox, skipPauses, maxDurationMs, maxEventCount, storyboard, essentialFeaturesOnly, debugger: enableStepThroughDebugger, }: RawReplayCommandHandlerOptions) => Promise<Replay>;
38
+ export declare const rawReplayCommandHandler: ({ apiToken, commitSha, sessionId, appUrl, simulationIdForAssets, headless, devTools, bypassCSP, screenshot, diffThreshold, diffPixelThreshold, shiftTime, networkStubbing, moveBeforeClick, cookiesFile, disableRemoteFonts, noSandbox, skipPauses, maxDurationMs, maxEventCount, storyboard, essentialFeaturesOnly, debugger: enableStepThroughDebugger, }: RawReplayCommandHandlerOptions) => Promise<Replay>;
40
39
  export declare const getReplayTarget: ({ appUrl, simulationIdForAssets, }: {
41
40
  appUrl: string | null;
42
41
  simulationIdForAssets: string | null;
@@ -134,10 +133,6 @@ export declare const replayCommand: import("yargs").CommandModule<unknown, impor
134
133
  description: string;
135
134
  default: boolean;
136
135
  };
137
- screenshotSelector: {
138
- string: true;
139
- description: string;
140
- };
141
136
  debugger: {
142
137
  boolean: true;
143
138
  description: string;
@@ -232,7 +232,7 @@ const serveOrGetAppUrl = async (client, replayTarget) => {
232
232
  const unknownReplayTargetType = (replayTarget) => {
233
233
  throw new Error(`Unknown type of replay target: ${JSON.stringify(replayTarget)}`);
234
234
  };
235
- const rawReplayCommandHandler = async ({ apiToken, commitSha, sessionId, appUrl, simulationIdForAssets, headless, devTools, bypassCSP, screenshot, screenshotSelector, diffThreshold, diffPixelThreshold, shiftTime, networkStubbing, moveBeforeClick, cookiesFile, disableRemoteFonts, noSandbox, skipPauses, maxDurationMs, maxEventCount, storyboard, essentialFeaturesOnly, debugger: enableStepThroughDebugger, }) => {
235
+ const rawReplayCommandHandler = async ({ apiToken, commitSha, sessionId, appUrl, simulationIdForAssets, headless, devTools, bypassCSP, screenshot, diffThreshold, diffPixelThreshold, shiftTime, networkStubbing, moveBeforeClick, cookiesFile, disableRemoteFonts, noSandbox, skipPauses, maxDurationMs, maxEventCount, storyboard, essentialFeaturesOnly, debugger: enableStepThroughDebugger, }) => {
236
236
  const executionOptions = {
237
237
  headless,
238
238
  devTools,
@@ -254,7 +254,6 @@ const rawReplayCommandHandler = async ({ apiToken, commitSha, sessionId, appUrl,
254
254
  const screenshottingOptions = screenshot
255
255
  ? {
256
256
  enabled: true,
257
- screenshotSelector: screenshotSelector !== null && screenshotSelector !== void 0 ? screenshotSelector : null,
258
257
  diffOptions: { diffPixelThreshold, diffThreshold },
259
258
  storyboardOptions,
260
259
  }
@@ -317,10 +316,6 @@ exports.replayCommand = (0, command_builder_1.buildCommand)("simulate")
317
316
  description: "Take a screenshot at the end of simulation",
318
317
  default: true,
319
318
  },
320
- screenshotSelector: {
321
- string: true,
322
- description: "Query selector to screenshot a specific DOM element instead of the whole page",
323
- },
324
319
  debugger: {
325
320
  boolean: true,
326
321
  description: "Opens a step through debugger to advance through the replay event by event",
@@ -97,16 +97,16 @@ export declare const runAllTestsCommand: import("yargs").CommandModule<unknown,
97
97
  readonly description: "Number of tasks to run in parallel (defaults to two per CPU)";
98
98
  readonly coerce: (value: number | null | undefined) => number | null | undefined;
99
99
  };
100
- readonly deflake: {
101
- readonly boolean: true;
102
- readonly description: "Attempt to deflake failing tests";
103
- readonly default: false;
104
- };
105
100
  readonly maxRetriesOnFailure: {
106
101
  readonly number: true;
107
102
  readonly description: "If set to a value greater than 0 then will re-run any replays that give a screenshot diff and mark them as a flake if the screenshot generated on one of the retryed replays differs from that in the first replay.";
108
103
  readonly default: 0;
109
104
  };
105
+ readonly rerunTestsNTimes: {
106
+ readonly number: true;
107
+ readonly description: "If set to a value greater than 0 then will re-run all replays the specified number of times and mark them as a flake if the screenshot generated on one of the retryed replays differs from that in the first replay.";
108
+ readonly default: 0;
109
+ };
110
110
  readonly useCache: {
111
111
  readonly boolean: true;
112
112
  readonly description: "Use result cache";
@@ -7,7 +7,7 @@ const command_builder_1 = require("../../command-utils/command-builder");
7
7
  const common_options_1 = require("../../command-utils/common-options");
8
8
  const run_all_tests_1 = require("../../parallel-tests/run-all-tests");
9
9
  const commit_sha_utils_1 = require("../../utils/commit-sha.utils");
10
- const handler = async ({ apiToken, commitSha: commitSha_, baseCommitSha, appUrl, headless, devTools, bypassCSP, diffThreshold, diffPixelThreshold, shiftTime, networkStubbing, githubSummary, parallelize, parallelTasks: parrelelTasks_, deflake, maxRetriesOnFailure, useCache, testsFile, disableRemoteFonts, noSandbox, skipPauses, moveBeforeClick, maxDurationMs, maxEventCount, storyboard, essentialFeaturesOnly, baseTestRunId, }) => {
10
+ const handler = async ({ apiToken, commitSha: commitSha_, baseCommitSha, appUrl, headless, devTools, bypassCSP, diffThreshold, diffPixelThreshold, shiftTime, networkStubbing, githubSummary, parallelize, parallelTasks: parrelelTasks_, maxRetriesOnFailure, rerunTestsNTimes, useCache, testsFile, disableRemoteFonts, noSandbox, skipPauses, moveBeforeClick, maxDurationMs, maxEventCount, storyboard, essentialFeaturesOnly, baseTestRunId, }) => {
11
11
  const executionOptions = {
12
12
  headless,
13
13
  devTools,
@@ -27,7 +27,6 @@ const handler = async ({ apiToken, commitSha: commitSha_, baseCommitSha, appUrl,
27
27
  : { enabled: false };
28
28
  const screenshottingOptions = {
29
29
  enabled: true,
30
- screenshotSelector: null,
31
30
  diffOptions: { diffPixelThreshold, diffThreshold },
32
31
  storyboardOptions,
33
32
  };
@@ -47,8 +46,8 @@ const handler = async ({ apiToken, commitSha: commitSha_, baseCommitSha, appUrl,
47
46
  baseTestRunId: baseTestRunId !== null && baseTestRunId !== void 0 ? baseTestRunId : null,
48
47
  appUrl: appUrl !== null && appUrl !== void 0 ? appUrl : null,
49
48
  parallelTasks: parrelelTasks !== null && parrelelTasks !== void 0 ? parrelelTasks : null,
50
- deflake,
51
49
  maxRetriesOnFailure,
50
+ rerunTestsNTimes,
52
51
  cachedTestRunResults,
53
52
  githubSummary,
54
53
  });
@@ -89,16 +88,16 @@ exports.runAllTestsCommand = (0, command_builder_1.buildCommand)("run-all-tests"
89
88
  return value;
90
89
  },
91
90
  },
92
- deflake: {
93
- boolean: true,
94
- description: "Attempt to deflake failing tests",
95
- default: false,
96
- },
97
91
  maxRetriesOnFailure: {
98
92
  number: true,
99
93
  description: "If set to a value greater than 0 then will re-run any replays that give a screenshot diff and mark them as a flake if the screenshot generated on one of the retryed replays differs from that in the first replay.",
100
94
  default: 0,
101
95
  },
96
+ rerunTestsNTimes: {
97
+ number: true,
98
+ description: "If set to a value greater than 0 then will re-run all replays the specified number of times and mark them as a flake if the screenshot generated on one of the retryed replays differs from that in the first replay.",
99
+ default: 0,
100
+ },
102
101
  useCache: {
103
102
  boolean: true,
104
103
  description: "Use result cache",
@@ -0,0 +1,25 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const get_screenshot_identifier_1 = require("../get-screenshot-identifier");
4
+ describe("get-screenshot-identifier", () => {
5
+ it("can parse identifiers without version numbers", () => {
6
+ expect((0, get_screenshot_identifier_1.getScreenshotIdentifier)("final-state.png")).toEqual({
7
+ type: "end-state",
8
+ });
9
+ expect((0, get_screenshot_identifier_1.getScreenshotIdentifier)("screenshot-after-event-7.png")).toEqual({
10
+ type: "after-event",
11
+ eventNumber: 7,
12
+ });
13
+ });
14
+ it("can parse identifiers with version numbers", () => {
15
+ expect((0, get_screenshot_identifier_1.getScreenshotIdentifier)("final-state-v2.png")).toEqual({
16
+ type: "end-state",
17
+ logicVersion: 2,
18
+ });
19
+ expect((0, get_screenshot_identifier_1.getScreenshotIdentifier)("screenshot-after-event-7-v2.png")).toEqual({
20
+ type: "after-event",
21
+ eventNumber: 7,
22
+ logicVersion: 2,
23
+ });
24
+ });
25
+ });
@@ -5,11 +5,15 @@ exports.getScreenshotFilename = void 0;
5
5
  // in replay-node, and in https://github.com/alwaysmeticulous/meticulous-sdk/blob/395af4394dc51d9b51ba1136fc26b23fcbba5604/packages/replayer/src/screenshot.utils.ts#L42
6
6
  const getScreenshotFilename = (identifier) => {
7
7
  if (identifier.type === "end-state") {
8
- return "final-state.png";
8
+ return identifier.logicVersion == null
9
+ ? "final-state.png"
10
+ : `final-state-v${identifier.logicVersion}.png`;
9
11
  }
10
12
  else if (identifier.type === "after-event") {
11
13
  const eventIndexStr = identifier.eventNumber.toString().padStart(5, "0");
12
- return `screenshot-after-event-${eventIndexStr}.png`;
14
+ return identifier.logicVersion == null
15
+ ? `screenshot-after-event-${eventIndexStr}.png`
16
+ : `screenshot-after-event-${eventIndexStr}-v${identifier.logicVersion}.png`;
13
17
  }
14
18
  else {
15
19
  throw new Error("Unexpected screenshot identifier: " + JSON.stringify(identifier));
@@ -3,19 +3,34 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.getScreenshotIdentifier = void 0;
4
4
  const path_1 = require("path");
5
5
  const getScreenshotIdentifier = (filename) => {
6
+ var _a;
6
7
  const name = (0, path_1.basename)(filename);
7
8
  if (name === "final-state.png") {
8
9
  return {
9
10
  type: "end-state",
10
11
  };
11
12
  }
13
+ if (name.startsWith("final-state-v")) {
14
+ const match = name.match(/^final-state-v(\d+)[.]png$/);
15
+ const logicVersionNumber = match ? parseInt(match[1], 10) : undefined;
16
+ if (match && logicVersionNumber != null && !isNaN(logicVersionNumber)) {
17
+ return {
18
+ type: "end-state",
19
+ logicVersion: logicVersionNumber,
20
+ };
21
+ }
22
+ }
12
23
  if (name.startsWith("screenshot-after-event")) {
13
- const match = name.match(/^(?:.*)-(\d+)[.]png$/);
24
+ const match = (_a = name.match(/^(?:.*)-(\d+)-v(\d+)?[.]png$/)) !== null && _a !== void 0 ? _a : name.match(/^(?:.*)-(\d+)[.]png$/);
14
25
  const eventNumber = match ? parseInt(match[1], 10) : undefined;
26
+ const logicVersionNumber = match ? parseInt(match[2], 10) : undefined;
15
27
  if (match && eventNumber != null && !isNaN(eventNumber)) {
16
28
  return {
17
29
  type: "after-event",
18
30
  eventNumber,
31
+ ...(logicVersionNumber != null && !isNaN(logicVersionNumber)
32
+ ? { logicVersion: logicVersionNumber }
33
+ : {}),
19
34
  };
20
35
  }
21
36
  }
@@ -22,10 +22,9 @@ const getConfigFilePath = async () => {
22
22
  return configFilePath;
23
23
  };
24
24
  const validateReplayOptions = (prevOptions) => {
25
- const { appUrl, screenshotSelector, diffThreshold, diffPixelThreshold, moveBeforeClick, simulationIdForAssets, } = prevOptions;
25
+ const { appUrl, diffThreshold, diffPixelThreshold, moveBeforeClick, simulationIdForAssets, } = prevOptions;
26
26
  return {
27
27
  ...(appUrl != null ? { appUrl } : {}),
28
- ...(screenshotSelector != null ? { screenshotSelector } : {}),
29
28
  ...(diffThreshold != null ? { diffThreshold } : {}),
30
29
  ...(diffPixelThreshold != null ? { diffPixelThreshold } : {}),
31
30
  ...(moveBeforeClick != null ? { moveBeforeClick } : {}),
@@ -12,14 +12,16 @@ describe("mergeResults", () => {
12
12
  });
13
13
  expect(mergedResult).toEqual((0, mock_test_results_1.testResult)("fail", [(0, mock_test_results_1.diff)(0), (0, mock_test_results_1.noDiff)(1)]));
14
14
  });
15
- it("ignores diffs to screenshots which originally passed", () => {
15
+ it("doesn't ignore diffs to screenshots which originally passed", () => {
16
16
  const currentResult = (0, mock_test_results_1.testResult)("pass", [(0, mock_test_results_1.noDiff)(0), (0, mock_test_results_1.noDiff)(1)]);
17
17
  const comparisonToHeadReplay = (0, mock_test_results_1.testResult)("fail", [(0, mock_test_results_1.noDiff)(0), (0, mock_test_results_1.diff)(1)]);
18
18
  const mergedResult = (0, merge_test_results_1.mergeResults)({
19
19
  currentResult,
20
20
  comparisonToHeadReplay,
21
21
  });
22
- expect(mergedResult).toEqual((0, mock_test_results_1.testResult)("pass", [(0, mock_test_results_1.noDiff)(0), (0, mock_test_results_1.noDiff)(1)]));
22
+ expect(mergedResult).toEqual(
23
+ // All screenshots are either "no-diff" or "flake" thus the overall result is "flake"
24
+ (0, mock_test_results_1.testResult)("flake", [(0, mock_test_results_1.noDiff)(0), (0, mock_test_results_1.flake)(1, (0, mock_test_results_1.noDiff)(), [(0, mock_test_results_1.diff)()])]));
23
25
  });
24
26
  it("marks screenshots as flakes if the screenshot comparison originally failed, but the second retry gives a different screenshot again", () => {
25
27
  const currentResult = (0, mock_test_results_1.testResult)("fail", [(0, mock_test_results_1.noDiff)(0), (0, mock_test_results_1.diff)(1), (0, mock_test_results_1.diff)(2)]);
@@ -17,6 +17,7 @@ describe("runAllTestsInParallel", () => {
17
17
  testsToRun: [testCase(0), testCase(1), testCase(2), testCase(3)],
18
18
  parallelTasks: 2,
19
19
  maxRetriesOnFailure: 0,
20
+ rerunTestsNTimes: 0,
20
21
  executeTest: async (testCase) => {
21
22
  var _a;
22
23
  testsStarted++;
@@ -77,6 +78,7 @@ describe("runAllTestsInParallel", () => {
77
78
  testsToRun: [testCase(0), testCase(1), testCase(2), testCase(3)],
78
79
  parallelTasks: 2,
79
80
  maxRetriesOnFailure: 0,
81
+ rerunTestsNTimes: 0,
80
82
  executeTest: async (testCase) => {
81
83
  var _a;
82
84
  await testRunPromises[parseInt((_a = testCase.title) !== null && _a !== void 0 ? _a : "")].promise;
@@ -109,6 +111,7 @@ describe("runAllTestsInParallel", () => {
109
111
  testsToRun: [testCase(0)],
110
112
  parallelTasks: 2,
111
113
  maxRetriesOnFailure: 0,
114
+ rerunTestsNTimes: 0,
112
115
  executeTest: async (testCase) => {
113
116
  return (0, mock_test_results_1.testResult)("fail", [(0, mock_test_results_1.diff)(0)], testCase);
114
117
  },
@@ -121,6 +124,7 @@ describe("runAllTestsInParallel", () => {
121
124
  testsToRun: [testCase(0)],
122
125
  parallelTasks: 2,
123
126
  maxRetriesOnFailure: 10,
127
+ rerunTestsNTimes: 0,
124
128
  executeTest: async (testCase, isRetry) => {
125
129
  if (!isRetry) {
126
130
  return (0, mock_test_results_1.testResult)("fail", [(0, mock_test_results_1.diff)(0), (0, mock_test_results_1.diff)(1)], testCase);
@@ -145,6 +149,7 @@ describe("runAllTestsInParallel", () => {
145
149
  testsToRun: [testCase(0)],
146
150
  parallelTasks: 2,
147
151
  maxRetriesOnFailure: 3,
152
+ rerunTestsNTimes: 0,
148
153
  executeTest: async (testCase, isRetry) => {
149
154
  if (!isRetry) {
150
155
  return (0, mock_test_results_1.testResult)("fail", [(0, mock_test_results_1.diff)(0), (0, mock_test_results_1.diff)(1)], testCase);
@@ -156,6 +161,31 @@ describe("runAllTestsInParallel", () => {
156
161
  (0, mock_test_results_1.testResult)("fail", [(0, mock_test_results_1.diff)(0), (0, mock_test_results_1.flake)(1, (0, mock_test_results_1.diff)(), [(0, mock_test_results_1.diff)(), (0, mock_test_results_1.diff)(), (0, mock_test_results_1.diff)()])], testCase(0)),
157
162
  ]);
158
163
  });
164
+ it("retries N times if rerunTestsNTimes > 0", async () => {
165
+ let rerunNum = 0;
166
+ const results = await (0, parallel_tests_handler_1.runAllTestsInParallel)({
167
+ testsToRun: [testCase(0)],
168
+ parallelTasks: 2,
169
+ maxRetriesOnFailure: 0,
170
+ rerunTestsNTimes: 3,
171
+ executeTest: async (testCase, isRetry) => {
172
+ if (!isRetry) {
173
+ return (0, mock_test_results_1.testResult)("pass", [(0, mock_test_results_1.diff)(0), (0, mock_test_results_1.noDiff)(1)], testCase);
174
+ }
175
+ if (rerunNum === 2) {
176
+ return (0, mock_test_results_1.testResult)("fail", [(0, mock_test_results_1.missingHead)(0), (0, mock_test_results_1.diff)(1)], testCase);
177
+ }
178
+ rerunNum++;
179
+ return (0, mock_test_results_1.testResult)("pass", [(0, mock_test_results_1.noDiff)(0), (0, mock_test_results_1.noDiff)(1)], testCase);
180
+ },
181
+ });
182
+ expect(rerunNum).toEqual(2);
183
+ expect(results).toEqual([
184
+ (0, mock_test_results_1.testResult)(
185
+ // Flake outcome is returned iff all screenshots are flaky.
186
+ "flake", [(0, mock_test_results_1.flake)(0, (0, mock_test_results_1.diff)(), [(0, mock_test_results_1.missingHead)()]), (0, mock_test_results_1.flake)(1, (0, mock_test_results_1.noDiff)(), [(0, mock_test_results_1.diff)()])], testCase(0)),
187
+ ]);
188
+ });
159
189
  });
160
190
  const testCase = (num) => ({
161
191
  sessionId: "mock-session-id",
@@ -1,4 +1,5 @@
1
- import { DetailedTestCaseResult } from "../config/config.types";
1
+ import { ScreenshotDiffResult } from "@alwaysmeticulous/api";
2
+ import { DetailedTestCaseResult, TestCaseResult } from "../config/config.types";
2
3
  export interface ResultsToMerge {
3
4
  currentResult: DetailedTestCaseResult;
4
5
  /**
@@ -8,3 +9,4 @@ export interface ResultsToMerge {
8
9
  comparisonToHeadReplay: DetailedTestCaseResult;
9
10
  }
10
11
  export declare const mergeResults: ({ currentResult, comparisonToHeadReplay, }: ResultsToMerge) => DetailedTestCaseResult;
12
+ export declare const testRunOutcomeFromDiffResults: (currentResult: TestCaseResult["result"], newScreenshotDiffResults: ScreenshotDiffResult[]) => TestCaseResult["result"];
@@ -3,9 +3,9 @@ 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.mergeResults = void 0;
7
- const utils_1 = require("@sentry/utils");
6
+ exports.testRunOutcomeFromDiffResults = exports.mergeResults = void 0;
8
7
  const fast_json_stable_stringify_1 = __importDefault(require("fast-json-stable-stringify"));
8
+ const has_notable_differences_1 = require("../commands/screenshot-diff/utils/has-notable-differences");
9
9
  const screenshot_diff_results_utils_1 = require("./screenshot-diff-results.utils");
10
10
  const mergeResults = ({ currentResult, comparisonToHeadReplay, }) => {
11
11
  // If any of the screenshots diffs in comparisonToHeadReplay show a diff against one
@@ -20,9 +20,6 @@ const mergeResults = ({ currentResult, comparisonToHeadReplay, }) => {
20
20
  retryDiffById.set(hash, result);
21
21
  });
22
22
  const newScreenshotDiffResults = (0, screenshot_diff_results_utils_1.flattenScreenshotDiffResults)(currentResult).map((currentDiff) => {
23
- if (currentDiff.outcome === "no-diff") {
24
- return currentDiff;
25
- }
26
23
  const hash = hashScreenshotIdentifier(currentDiff.identifier);
27
24
  const diffWhenRetrying = retryDiffById.get(hash);
28
25
  // diffWhenRetrying is null in the case that there is a base screenshot for the base replay,
@@ -75,12 +72,9 @@ const mergeResults = ({ currentResult, comparisonToHeadReplay, }) => {
75
72
  };
76
73
  }
77
74
  });
78
- const noLongerHasFailures = newScreenshotDiffResults.every(({ outcome }) => outcome === "flake" || outcome === "no-diff");
79
75
  return {
80
76
  ...currentResult,
81
- result: currentResult.result === "fail" && noLongerHasFailures
82
- ? "flake"
83
- : currentResult.result,
77
+ result: (0, exports.testRunOutcomeFromDiffResults)(currentResult.result, newScreenshotDiffResults),
84
78
  screenshotDiffResultsByBaseReplayId: (0, screenshot_diff_results_utils_1.groupScreenshotDiffResults)(newScreenshotDiffResults),
85
79
  };
86
80
  };
@@ -91,20 +85,22 @@ identifier: _identifier,
91
85
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
92
86
  baseReplayId: _baseReplayId, ...rest }) => rest;
93
87
  const hashScreenshotIdentifier = (identifier) => {
94
- if (identifier.type === "end-state") {
95
- return "end-state";
96
- }
97
- else if (identifier.type === "after-event") {
98
- return `after-event-${identifier.eventNumber}`;
88
+ return (0, fast_json_stable_stringify_1.default)(identifier);
89
+ };
90
+ const testRunOutcomeFromDiffResults = (currentResult, newScreenshotDiffResults) => {
91
+ // If a test run is already flakey, we don't want to overwrite that with a 'fail' result.
92
+ if (currentResult === "flake") {
93
+ return "flake";
99
94
  }
100
- else {
101
- unknownScreenshotIdentifierType(identifier);
102
- // The identifier is probably from a newer version of the bundle script
103
- // and we're on an old version of the CLI. Our best bet is to stringify it
104
- // and use that as a hash.
105
- return (0, fast_json_stable_stringify_1.default)(identifier);
95
+ const hasOnlyFlakes = newScreenshotDiffResults.every(({ outcome }) => outcome === "flake" || outcome === "no-diff");
96
+ if (hasOnlyFlakes) {
97
+ return "flake";
106
98
  }
99
+ // If we've had a test run has already failed, we don't want to overwrite that with a 'pass' result.
100
+ // Otherwise, if there are any notable differences, we want to fail the test run.
101
+ return currentResult === "fail" ||
102
+ (0, has_notable_differences_1.hasNotableDifferences)(newScreenshotDiffResults)
103
+ ? "fail"
104
+ : "pass";
107
105
  };
108
- const unknownScreenshotIdentifierType = (identifier) => {
109
- utils_1.logger.error(`Unknown type of screenshot identifier: ${JSON.stringify(identifier)}`);
110
- };
106
+ exports.testRunOutcomeFromDiffResults = testRunOutcomeFromDiffResults;
@@ -1,12 +1,12 @@
1
1
  import log from "loglevel";
2
2
  import { DetailedTestCaseResult } from "../config/config.types";
3
- import { DeflakeReplayCommandHandlerOptions } from "../deflake-tests/deflake-tests.handler";
3
+ import { ParallelTestsReplayOptions } from "./parallel-replay.types";
4
4
  export interface InitMessage {
5
5
  kind: "init";
6
6
  data: {
7
7
  logLevel: log.LogLevel[keyof log.LogLevel];
8
8
  dataDir: string;
9
- replayOptions: DeflakeReplayCommandHandlerOptions;
9
+ replayOptions: ParallelTestsReplayOptions;
10
10
  };
11
11
  }
12
12
  export interface ResultMessage {
@@ -0,0 +1,3 @@
1
+ import { DetailedTestCaseResult } from "../config/config.types";
2
+ import { ParallelTestsReplayOptions } from "./parallel-replay.types";
3
+ export declare const handleReplay: ({ testCase, replayTarget, executionOptions, screenshottingOptions, apiToken, commitSha, generatedBy, testRunId, replayEventsDependencies, suppressScreenshotDiffLogging, baseTestRunId, }: ParallelTestsReplayOptions) => Promise<DetailedTestCaseResult>;
@@ -1,11 +1,6 @@
1
1
  "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
2
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.deflakeReplayCommandHandler = void 0;
7
- const common_1 = require("@alwaysmeticulous/common");
8
- const loglevel_1 = __importDefault(require("loglevel"));
3
+ exports.handleReplay = void 0;
9
4
  const replay_command_1 = require("../commands/replay/replay.command");
10
5
  const has_notable_differences_1 = require("../commands/screenshot-diff/utils/has-notable-differences");
11
6
  const handleReplay = async ({ testCase, replayTarget, executionOptions, screenshottingOptions, apiToken, commitSha, generatedBy, testRunId, replayEventsDependencies, suppressScreenshotDiffLogging, baseTestRunId, }) => {
@@ -35,21 +30,7 @@ const handleReplay = async ({ testCase, replayTarget, executionOptions, screensh
35
30
  screenshotDiffResultsByBaseReplayId,
36
31
  };
37
32
  };
38
- const deflakeReplayCommandHandler = async ({ deflake, ...otherOptions }) => {
39
- const logger = loglevel_1.default.getLogger(common_1.METICULOUS_LOGGER_NAME);
40
- const firstResult = await handleReplay(otherOptions);
41
- if (firstResult.result === "pass" || !deflake) {
42
- return firstResult;
43
- }
44
- const secondResult = await handleReplay(otherOptions);
45
- if (secondResult.result === "fail") {
46
- return secondResult;
47
- }
48
- const thirdResult = await handleReplay(otherOptions);
49
- logger.info(`FLAKE: ${thirdResult.title} => ${thirdResult.result}`);
50
- return thirdResult;
51
- };
52
- exports.deflakeReplayCommandHandler = deflakeReplayCommandHandler;
33
+ exports.handleReplay = handleReplay;
53
34
  const applyTestCaseExecutionOptionOverrides = (executionOptionsFromCliFlags, overridesFromTestCase) => {
54
35
  var _a;
55
36
  // Options specified in the test case override those passed as CLI flags
@@ -60,7 +41,7 @@ const applyTestCaseExecutionOptionOverrides = (executionOptionsFromCliFlags, ove
60
41
  };
61
42
  };
62
43
  const applyTestCaseScreenshottingOptionsOverrides = (screenshottingOptionsFromCliFlags, overridesFromTestCase) => {
63
- var _a, _b, _c;
44
+ var _a, _b;
64
45
  // Options specified in the test case override those passed as CLI flags
65
46
  // (CLI flags set the defaults)
66
47
  const diffOptions = {
@@ -69,7 +50,6 @@ const applyTestCaseScreenshottingOptionsOverrides = (screenshottingOptionsFromCl
69
50
  };
70
51
  return {
71
52
  enabled: true,
72
- screenshotSelector: (_c = overridesFromTestCase.screenshotSelector) !== null && _c !== void 0 ? _c : screenshottingOptionsFromCliFlags.screenshotSelector,
73
53
  diffOptions,
74
54
  storyboardOptions: screenshottingOptionsFromCliFlags.storyboardOptions,
75
55
  };
@@ -1,11 +1,7 @@
1
1
  import { TestCase } from "@alwaysmeticulous/api";
2
2
  import { GeneratedBy, ReplayEventsDependencies, ReplayExecutionOptions, ReplayTarget } from "@alwaysmeticulous/common";
3
3
  import { ScreenshotAssertionsEnabledOptions } from "../command-utils/common-types";
4
- import { DetailedTestCaseResult } from "../config/config.types";
5
- export interface DeflakeReplayCommandHandlerOptions extends HandleReplayOptions {
6
- deflake: boolean;
7
- }
8
- interface HandleReplayOptions {
4
+ export interface ParallelTestsReplayOptions {
9
5
  replayTarget: ReplayTarget;
10
6
  executionOptions: ReplayExecutionOptions;
11
7
  screenshottingOptions: ScreenshotAssertionsEnabledOptions;
@@ -18,5 +14,3 @@ interface HandleReplayOptions {
18
14
  replayEventsDependencies: ReplayEventsDependencies;
19
15
  suppressScreenshotDiffLogging: boolean;
20
16
  }
21
- export declare const deflakeReplayCommandHandler: ({ deflake, ...otherOptions }: DeflakeReplayCommandHandlerOptions) => Promise<DetailedTestCaseResult>;
22
- export {};
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -5,6 +5,7 @@ export interface RunAllTestsInParallelOptions {
5
5
  testsToRun: TestCase[];
6
6
  parallelTasks: number | null;
7
7
  maxRetriesOnFailure: number;
8
+ rerunTestsNTimes: number;
8
9
  executeTest: (testCase: TestCase, isRetry: boolean) => Promise<DetailedTestCaseResult>;
9
10
  onTestFinished?: (progress: TestRunProgress, resultsSoFar: DetailedTestCaseResult[]) => Promise<void>;
10
11
  onTestFailedToRun?: (progress: TestRunProgress) => Promise<void>;
@@ -10,7 +10,10 @@ const loglevel_1 = __importDefault(require("loglevel"));
10
10
  const merge_test_results_1 = require("./merge-test-results");
11
11
  const screenshot_diff_results_utils_1 = require("./screenshot-diff-results.utils");
12
12
  /** Handler for running Meticulous tests in parallel using child processes */
13
- const runAllTestsInParallel = async ({ testsToRun, parallelTasks, maxRetriesOnFailure, executeTest, onTestFinished, onTestFailedToRun, }) => {
13
+ const runAllTestsInParallel = async ({ testsToRun, parallelTasks, maxRetriesOnFailure, rerunTestsNTimes, executeTest, onTestFinished, onTestFailedToRun, }) => {
14
+ if (maxRetriesOnFailure && rerunTestsNTimes) {
15
+ throw new Error("maxRetriesOnFailure and rerunTestsNTimes are mutually exclusive.");
16
+ }
14
17
  const logger = loglevel_1.default.getLogger(common_1.METICULOUS_LOGGER_NAME);
15
18
  let nextId = 0;
16
19
  let queue = testsToRun.map((test) => ({
@@ -47,8 +50,16 @@ const runAllTestsInParallel = async ({ testsToRun, parallelTasks, maxRetriesOnFa
47
50
  .then(async (result) => {
48
51
  var _a;
49
52
  const resultsForTestCase = resultsByTestId.get(id);
53
+ // Re-run the test if needed, comparing to the base replay.
54
+ if (!isRetry) {
55
+ queue.push(...Array.from(new Array(rerunTestsNTimes)).map(() => ({
56
+ ...testCase,
57
+ id,
58
+ baseReplayId: result === null || result === void 0 ? void 0 : result.headReplayId,
59
+ })));
60
+ }
50
61
  if (resultsForTestCase != null && result != null) {
51
- logRetrySummary(testName({ id, ...testCase }), result);
62
+ logRetrySummary(testName({ id, ...testCase }), result, resultsForTestCase.currentResult);
52
63
  }
53
64
  if (resultsForTestCase != null &&
54
65
  resultsForTestCase.currentResult.result === "flake") {
@@ -81,8 +92,15 @@ const runAllTestsInParallel = async ({ testsToRun, parallelTasks, maxRetriesOnFa
81
92
  // Our work is done for this test case if the first result was a pass,
82
93
  // we've performed all the retries, or one of the retries already proved
83
94
  // the result as flakey
84
- const isFinalResult = mergedResult.result !== "fail" ||
85
- numberOfRetriesExecuted === maxRetriesOnFailure;
95
+ let isFinalResult;
96
+ if (rerunTestsNTimes > 0) {
97
+ isFinalResult = numberOfRetriesExecuted >= rerunTestsNTimes;
98
+ }
99
+ else {
100
+ isFinalResult =
101
+ mergedResult.result !== "fail" ||
102
+ numberOfRetriesExecuted >= maxRetriesOnFailure;
103
+ }
86
104
  if (isFinalResult) {
87
105
  // Cancel any replays that are still scheduled
88
106
  queue = queue.filter((otherReplay) => otherReplay.id !== id);
@@ -119,7 +137,12 @@ const runAllTestsInParallel = async ({ testsToRun, parallelTasks, maxRetriesOnFa
119
137
  }
120
138
  ++inProgress;
121
139
  if (resultsByTestId.has(testCase.id)) {
122
- logger.info(`Test ${testName(testCase)} failed. Retrying to check for flakes...`);
140
+ if (maxRetriesOnFailure > 0) {
141
+ logger.info(`Test ${testName(testCase)} failed. Retrying to check for flakes...`);
142
+ }
143
+ else if (rerunTestsNTimes > 0) {
144
+ logger.info(`Re-running ${testName(testCase)} to check for flakes...`);
145
+ }
123
146
  }
124
147
  startTask(testCase);
125
148
  process.nextTick(checkNextTask);
@@ -129,10 +152,10 @@ const runAllTestsInParallel = async ({ testsToRun, parallelTasks, maxRetriesOnFa
129
152
  return finalResults;
130
153
  };
131
154
  exports.runAllTestsInParallel = runAllTestsInParallel;
132
- const logRetrySummary = (nameOfTest, retryResult) => {
155
+ const logRetrySummary = (nameOfTest, retryResult, resultSoFar) => {
133
156
  const logger = loglevel_1.default.getLogger(common_1.METICULOUS_LOGGER_NAME);
134
157
  if (retryResult.result === "pass") {
135
- logger.info(`Retried taking screenshots for failed test ${nameOfTest}, but got the same results`);
158
+ logger.info(`Retried taking screenshots for${resultSoFar.result == "fail" ? " failed" : ""} test ${nameOfTest}, but got the same results`);
136
159
  }
137
160
  else {
138
161
  const numDifferingScreenshots = (0, screenshot_diff_results_utils_1.flattenScreenshotDiffResults)(retryResult).filter((result) => result.outcome !== "no-diff").length;
@@ -25,13 +25,22 @@ export interface Options {
25
25
  * Set to 1 to disable parralelism.
26
26
  */
27
27
  parallelTasks: number | null;
28
- deflake: boolean;
29
28
  /**
30
29
  * If set to a value greater than 1 then will re-run any replays that give a screenshot diff
31
30
  * and mark them as a flake if the screenshot generated on one of the retryed replays differs from that
32
31
  * in the first replay.
33
32
  */
34
33
  maxRetriesOnFailure: number;
34
+ /**
35
+ * If set to a value greater than 0 then will re-run all replays the specified number of times
36
+ * and mark them as a flake if the screenshot generated on one of the retryed replays differs from that
37
+ * in the first replay.
38
+ *
39
+ * This is useful for checking flake rates.
40
+ *
41
+ * This option is mutually exclusive with maxRetriesOnFailure.
42
+ */
43
+ rerunTestsNTimes: number;
35
44
  githubSummary: boolean;
36
45
  /**
37
46
  * If provided it will incorportate the cachedTestRunResults in any calls to store
@@ -61,4 +70,4 @@ export interface RunAllTestsResult {
61
70
  * Runs all the test cases in the provided file.
62
71
  * @returns The results of the tests that were executed (note that this does not include results from any cachedTestRunResults passed in)
63
72
  */
64
- export declare const runAllTests: ({ testsFile, apiToken, commitSha, baseCommitSha, appUrl, executionOptions, screenshottingOptions, parallelTasks, deflake, maxRetriesOnFailure, cachedTestRunResults: cachedTestRunResults_, githubSummary, environment, baseTestRunId, onTestRunCreated, onTestFinished: onTestFinished_, }: Options) => Promise<RunAllTestsResult>;
73
+ export declare const runAllTests: ({ testsFile, apiToken, commitSha, baseCommitSha, appUrl, executionOptions, screenshottingOptions, parallelTasks, maxRetriesOnFailure, rerunTestsNTimes, cachedTestRunResults: cachedTestRunResults_, githubSummary, environment, baseTestRunId, onTestRunCreated, onTestFinished: onTestFinished_, }: Options) => Promise<RunAllTestsResult>;
@@ -12,21 +12,18 @@ const replay_diff_api_1 = require("../api/replay-diff.api");
12
12
  const test_run_api_1 = require("../api/test-run.api");
13
13
  const config_1 = require("../config/config");
14
14
  const replay_assets_1 = require("../local-data/replay-assets");
15
- const parallel_tests_handler_1 = require("../parallel-tests/parallel-tests.handler");
16
15
  const config_utils_1 = require("../utils/config.utils");
17
16
  const github_summary_utils_1 = require("../utils/github-summary.utils");
18
17
  const run_all_tests_utils_1 = require("../utils/run-all-tests.utils");
19
18
  const test_run_environment_utils_1 = require("../utils/test-run-environment.utils");
20
19
  const version_utils_1 = require("../utils/version.utils");
21
20
  const execute_test_in_child_process_1 = require("./execute-test-in-child-process");
21
+ const parallel_tests_handler_1 = require("./parallel-tests.handler");
22
22
  /**
23
23
  * Runs all the test cases in the provided file.
24
24
  * @returns The results of the tests that were executed (note that this does not include results from any cachedTestRunResults passed in)
25
25
  */
26
- const runAllTests = async ({ testsFile, apiToken, commitSha, baseCommitSha, appUrl, executionOptions, screenshottingOptions, parallelTasks, deflake, maxRetriesOnFailure, cachedTestRunResults: cachedTestRunResults_, githubSummary, environment, baseTestRunId, onTestRunCreated, onTestFinished: onTestFinished_, }) => {
27
- if (deflake && maxRetriesOnFailure > 1) {
28
- throw new Error("Arguments deflake and maxRetriesOnFailure are mutually exclusive");
29
- }
26
+ const runAllTests = async ({ testsFile, apiToken, commitSha, baseCommitSha, appUrl, executionOptions, screenshottingOptions, parallelTasks, maxRetriesOnFailure, rerunTestsNTimes, cachedTestRunResults: cachedTestRunResults_, githubSummary, environment, baseTestRunId, onTestRunCreated, onTestFinished: onTestFinished_, }) => {
30
27
  const logger = loglevel_1.default.getLogger(common_1.METICULOUS_LOGGER_NAME);
31
28
  const client = (0, client_1.createClient)({ apiToken });
32
29
  const project = await (0, project_api_1.getProject)(client);
@@ -60,7 +57,6 @@ const runAllTests = async ({ testsFile, apiToken, commitSha, baseCommitSha, appU
60
57
  baseCommitSha,
61
58
  appUrl,
62
59
  parallelTasks,
63
- deflake,
64
60
  githubSummary,
65
61
  },
66
62
  environment: (0, test_run_environment_utils_1.getEnvironment)(environment),
@@ -145,6 +141,7 @@ const runAllTests = async ({ testsFile, apiToken, commitSha, baseCommitSha, appU
145
141
  testsToRun,
146
142
  parallelTasks,
147
143
  maxRetriesOnFailure,
144
+ rerunTestsNTimes,
148
145
  executeTest: (testCase, isRetry) => {
149
146
  var _a;
150
147
  const initMessage = {
@@ -156,7 +153,6 @@ const runAllTests = async ({ testsFile, apiToken, commitSha, baseCommitSha, appU
156
153
  apiToken,
157
154
  commitSha,
158
155
  testCase,
159
- deflake,
160
156
  replayTarget: (0, config_utils_1.getReplayTargetForTestCase)({
161
157
  appUrl,
162
158
  testCase,
@@ -30,9 +30,9 @@ const common_1 = require("@alwaysmeticulous/common");
30
30
  const Sentry = __importStar(require("@sentry/node"));
31
31
  const loglevel_1 = __importDefault(require("loglevel"));
32
32
  const luxon_1 = require("luxon");
33
- const deflake_tests_handler_1 = require("../deflake-tests/deflake-tests.handler");
34
33
  const logger_utils_1 = require("../utils/logger.utils");
35
34
  const sentry_utils_1 = require("../utils/sentry.utils");
35
+ const parallel_replay_handler_1 = require("./parallel-replay.handler");
36
36
  const INIT_TIMEOUT = luxon_1.Duration.fromObject({ second: 1 });
37
37
  const waitForInitMessage = () => {
38
38
  return Promise.race([
@@ -67,7 +67,7 @@ const main = async () => {
67
67
  const { logLevel, dataDir, replayOptions } = initMessage.data;
68
68
  logger.setLevel(logLevel);
69
69
  (0, common_1.setMeticulousLocalDataDir)(dataDir);
70
- const result = await (0, deflake_tests_handler_1.deflakeReplayCommandHandler)(replayOptions);
70
+ const result = await (0, parallel_replay_handler_1.handleReplay)(replayOptions);
71
71
  const resultMessage = {
72
72
  kind: "result",
73
73
  data: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alwaysmeticulous/cli",
3
- "version": "2.38.0",
3
+ "version": "2.40.0",
4
4
  "description": "The Meticulous CLI",
5
5
  "license": "ISC",
6
6
  "main": "dist/index.js",
@@ -26,7 +26,7 @@
26
26
  "test": "jest"
27
27
  },
28
28
  "dependencies": {
29
- "@alwaysmeticulous/common": "^2.38.0",
29
+ "@alwaysmeticulous/common": "^2.40.0",
30
30
  "@sentry/node": "^7.36.0",
31
31
  "@sentry/tracing": "^7.36.0",
32
32
  "adm-zip": "^0.5.9",
@@ -45,7 +45,7 @@
45
45
  "yargs": "^17.5.1"
46
46
  },
47
47
  "devDependencies": {
48
- "@alwaysmeticulous/api": "^2.37.0",
48
+ "@alwaysmeticulous/api": "^2.40.0",
49
49
  "@types/express": "^4.17.14",
50
50
  "@types/proper-lockfile": "^4.1.2"
51
51
  },
@@ -95,5 +95,5 @@
95
95
  "coverageDirectory": "../coverage",
96
96
  "testEnvironment": "node"
97
97
  },
98
- "gitHead": "2c4fbc9225d5e0dc09f14a1e90a67008a5e3f376"
98
+ "gitHead": "46c8a774b634b1e43ca8e792a6ed573adba296ef"
99
99
  }